Добавил слой Infrastructure
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
|
public DbSet<StudentProfile> StudentProfiles { get; set; } = null!;
|
||||||
|
public DbSet<TeacherProfile> TeacherProfiles { get; set; } = null!;
|
||||||
|
public DbSet<Course> Courses { get; set; } = null!;
|
||||||
|
public DbSet<Lecture> Lectures { get; set; } = null!;
|
||||||
|
public DbSet<Location> Locations { get; set; } = null!;
|
||||||
|
public DbSet<Tag> Tags { get; set; } = null!;
|
||||||
|
public DbSet<CourseTag> CourseTags { get; set; } = null!;
|
||||||
|
public DbSet<LectureEnrollment> LectureEnrollments { get; set; } = null!;
|
||||||
|
public DbSet<Review> Reviews { get; set; } = null!;
|
||||||
|
public DbSet<Achievement> Achievements { get; set; } = null!;
|
||||||
|
public DbSet<UserAchievement> UserAchievements { get; set; } = null!;
|
||||||
|
public DbSet<CoinTransaction> CoinTransactions { get; set; } = null!;
|
||||||
|
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
||||||
|
|
||||||
|
static AppDbContext()
|
||||||
|
{
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<UserRole>("user_role");
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<TagType>("tag_type");
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<ReviewLlmStatus>("review_llm_status");
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<ReviewSentiment>("review_sentiment");
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<CoinTransactionType>("coin_transaction_type");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.HasPostgresEnum<UserRole>("user_role");
|
||||||
|
modelBuilder.HasPostgresEnum<TagType>("tag_type");
|
||||||
|
modelBuilder.HasPostgresEnum<ReviewLlmStatus>("review_llm_status");
|
||||||
|
modelBuilder.HasPostgresEnum<ReviewSentiment>("review_sentiment");
|
||||||
|
modelBuilder.HasPostgresEnum<CoinTransactionType>("coin_transaction_type");
|
||||||
|
|
||||||
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||||
|
|
||||||
|
// Basic PK setups if they weren't fully in assembly
|
||||||
|
modelBuilder.Entity<CourseTag>().HasKey(ct => new { ct.CourseId, ct.TagId });
|
||||||
|
modelBuilder.Entity<LectureEnrollment>().HasKey(le => new { le.LectureId, le.UserId });
|
||||||
|
modelBuilder.Entity<UserAchievement>().HasKey(ua => new { ua.UserId, ua.AchievementId });
|
||||||
|
|
||||||
|
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||||
|
{
|
||||||
|
entity.SetTableName(entity.GetTableName()?.ToLowerInvariant());
|
||||||
|
foreach (var property in entity.GetProperties())
|
||||||
|
{
|
||||||
|
property.SetColumnName(string.Concat(property.Name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLowerInvariant());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class AchievementConfiguration : IEntityTypeConfiguration<Achievement>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Achievement> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("achievements");
|
||||||
|
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
builder.Property(a => a.Id).HasColumnName("id");
|
||||||
|
builder.Property(a => a.Name).HasColumnName("name").HasMaxLength(255).IsRequired();
|
||||||
|
builder.Property(a => a.Description).HasColumnName("description");
|
||||||
|
builder.Property(a => a.IconUrl).HasColumnName("icon_url").HasMaxLength(500);
|
||||||
|
builder.Property(a => a.XpReward).HasColumnName("xp_reward").HasDefaultValue(0);
|
||||||
|
builder.Property(a => a.CoinReward).HasColumnName("coin_reward").HasDefaultValue(0);
|
||||||
|
builder.Property(a => a.Condition).HasColumnName("condition");
|
||||||
|
builder.Property(a => a.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class CoinTransactionConfiguration : IEntityTypeConfiguration<CoinTransaction>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<CoinTransaction> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("coin_transactions");
|
||||||
|
|
||||||
|
builder.HasKey(ct => ct.Id);
|
||||||
|
builder.Property(ct => ct.Id).HasColumnName("id");
|
||||||
|
builder.Property(ct => ct.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(ct => ct.Amount).HasColumnName("amount");
|
||||||
|
builder.Property(ct => ct.Type).HasColumnName("type");
|
||||||
|
builder.Property(ct => ct.ReviewId).HasColumnName("review_id");
|
||||||
|
builder.Property(ct => ct.AchievementId).HasColumnName("achievement_id");
|
||||||
|
builder.Property(ct => ct.Description).HasColumnName("description").HasMaxLength(500);
|
||||||
|
builder.Property(ct => ct.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasOne(ct => ct.User)
|
||||||
|
.WithMany(u => u.CoinTransactions)
|
||||||
|
.HasForeignKey(ct => ct.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(ct => ct.Review)
|
||||||
|
.WithMany(r => r.CoinTransactions)
|
||||||
|
.HasForeignKey(ct => ct.ReviewId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
builder.HasOne(ct => ct.Achievement)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(ct => ct.AchievementId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
builder.HasIndex(ct => ct.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class CourseConfiguration : IEntityTypeConfiguration<Course>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Course> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("courses");
|
||||||
|
|
||||||
|
builder.HasKey(c => c.Id);
|
||||||
|
builder.Property(c => c.Id).HasColumnName("id");
|
||||||
|
builder.Property(c => c.Name).HasColumnName("name").HasMaxLength(500).IsRequired();
|
||||||
|
builder.Property(c => c.Description).HasColumnName("description");
|
||||||
|
builder.Property(c => c.ExternalId).HasColumnName("external_id").HasMaxLength(255);
|
||||||
|
builder.Property(c => c.IsSynced).HasColumnName("is_synced").HasDefaultValue(false);
|
||||||
|
builder.Property(c => c.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
builder.Property(c => c.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasIndex(c => c.ExternalId).IsUnique().HasFilter("external_id IS NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class CourseTagConfiguration : IEntityTypeConfiguration<CourseTag>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<CourseTag> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("course_tags");
|
||||||
|
|
||||||
|
builder.HasKey(ct => ct.Id);
|
||||||
|
builder.Property(ct => ct.Id).HasColumnName("id");
|
||||||
|
builder.Property(ct => ct.CourseId).HasColumnName("course_id");
|
||||||
|
builder.Property(ct => ct.TagId).HasColumnName("tag_id");
|
||||||
|
|
||||||
|
builder.HasOne(ct => ct.Course)
|
||||||
|
.WithMany(c => c.CourseTags)
|
||||||
|
.HasForeignKey(ct => ct.CourseId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(ct => ct.Tag)
|
||||||
|
.WithMany(t => t.CourseTags)
|
||||||
|
.HasForeignKey(ct => ct.TagId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(ct => new { ct.CourseId, ct.TagId }).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class LectureConfiguration : IEntityTypeConfiguration<Lecture>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Lecture> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("lectures");
|
||||||
|
|
||||||
|
builder.HasKey(l => l.Id);
|
||||||
|
builder.Property(l => l.Id).HasColumnName("id");
|
||||||
|
builder.Property(l => l.CourseId).HasColumnName("course_id");
|
||||||
|
builder.Property(l => l.TeacherId).HasColumnName("teacher_id");
|
||||||
|
builder.Property(l => l.LocationId).HasColumnName("location_id");
|
||||||
|
builder.Property(l => l.Title).HasColumnName("title").HasMaxLength(500).IsRequired();
|
||||||
|
builder.Property(l => l.Description).HasColumnName("description");
|
||||||
|
builder.Property(l => l.Format).HasColumnName("format");
|
||||||
|
builder.Property(l => l.StartsAt).HasColumnName("starts_at");
|
||||||
|
builder.Property(l => l.EndsAt).HasColumnName("ends_at");
|
||||||
|
builder.Property(l => l.IsOpen).HasColumnName("is_open").HasDefaultValue(true);
|
||||||
|
builder.Property(l => l.MaxEnrollments).HasColumnName("max_enrollments").HasDefaultValue(0);
|
||||||
|
builder.Property(l => l.ExternalId).HasColumnName("external_id").HasMaxLength(255);
|
||||||
|
builder.Property(l => l.OnlineUrl).HasColumnName("online_url").HasMaxLength(500);
|
||||||
|
builder.Property(l => l.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
builder.Property(l => l.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasOne(l => l.Course)
|
||||||
|
.WithMany(c => c.Lectures)
|
||||||
|
.HasForeignKey(l => l.CourseId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(l => l.Teacher)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(l => l.TeacherId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
builder.HasOne(l => l.Location)
|
||||||
|
.WithMany(loc => loc.Lectures)
|
||||||
|
.HasForeignKey(l => l.LocationId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
builder.HasIndex(l => l.ExternalId).IsUnique().HasFilter("external_id IS NOT NULL");
|
||||||
|
builder.HasIndex(l => l.StartsAt);
|
||||||
|
builder.HasIndex(l => l.CourseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class LectureEnrollmentConfiguration : IEntityTypeConfiguration<LectureEnrollment>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<LectureEnrollment> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("lecture_enrollments");
|
||||||
|
|
||||||
|
builder.HasKey(le => le.Id);
|
||||||
|
builder.Property(le => le.Id).HasColumnName("id");
|
||||||
|
builder.Property(le => le.LectureId).HasColumnName("lecture_id");
|
||||||
|
builder.Property(le => le.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(le => le.Attended).HasColumnName("attended").HasDefaultValue(false);
|
||||||
|
builder.Property(le => le.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasOne(le => le.Lecture)
|
||||||
|
.WithMany(l => l.Enrollments)
|
||||||
|
.HasForeignKey(le => le.LectureId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(le => le.User)
|
||||||
|
.WithMany(u => u.Enrollments)
|
||||||
|
.HasForeignKey(le => le.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(le => new { le.LectureId, le.UserId }).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class LocationConfiguration : IEntityTypeConfiguration<Location>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Location> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("locations");
|
||||||
|
|
||||||
|
builder.HasKey(l => l.Id);
|
||||||
|
builder.Property(l => l.Id).HasColumnName("id");
|
||||||
|
builder.Property(l => l.Name).HasColumnName("name").HasMaxLength(255).IsRequired();
|
||||||
|
builder.Property(l => l.Building).HasColumnName("building").HasMaxLength(255);
|
||||||
|
builder.Property(l => l.Room).HasColumnName("room").HasMaxLength(100);
|
||||||
|
builder.Property(l => l.Address).HasColumnName("address").HasMaxLength(500);
|
||||||
|
builder.Property(l => l.ExternalId).HasColumnName("external_id").HasMaxLength(255);
|
||||||
|
builder.Property(l => l.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasIndex(l => l.ExternalId).IsUnique().HasFilter("external_id IS NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class RefreshTokenConfiguration : IEntityTypeConfiguration<RefreshToken>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<RefreshToken> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("refresh_tokens");
|
||||||
|
|
||||||
|
builder.HasKey(rt => rt.Id);
|
||||||
|
builder.Property(rt => rt.Id).HasColumnName("id");
|
||||||
|
builder.Property(rt => rt.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(rt => rt.Token).HasColumnName("token").HasMaxLength(500).IsRequired();
|
||||||
|
builder.Property(rt => rt.ExpiresAt).HasColumnName("expires_at");
|
||||||
|
builder.Property(rt => rt.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
builder.Property(rt => rt.RevokedAt).HasColumnName("revoked_at");
|
||||||
|
|
||||||
|
builder.HasOne(rt => rt.User)
|
||||||
|
.WithMany(u => u.RefreshTokens)
|
||||||
|
.HasForeignKey(rt => rt.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(rt => rt.Token).IsUnique();
|
||||||
|
builder.HasIndex(rt => rt.UserId);
|
||||||
|
|
||||||
|
// Computed properties should be ignored by EF
|
||||||
|
builder.Ignore(rt => rt.IsExpired);
|
||||||
|
builder.Ignore(rt => rt.IsRevoked);
|
||||||
|
builder.Ignore(rt => rt.IsActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class ReviewConfiguration : IEntityTypeConfiguration<Review>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Review> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("reviews");
|
||||||
|
|
||||||
|
builder.HasKey(r => r.Id);
|
||||||
|
builder.Property(r => r.Id).HasColumnName("id");
|
||||||
|
builder.Property(r => r.LectureId).HasColumnName("lecture_id");
|
||||||
|
builder.Property(r => r.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(r => r.Rating).HasColumnName("rating");
|
||||||
|
builder.Property(r => r.Text).HasColumnName("text");
|
||||||
|
builder.Property(r => r.LlmStatus).HasColumnName("llm_status").HasDefaultValue(Domain.Enums.ReviewLlmStatus.Pending);
|
||||||
|
builder.Property(r => r.Sentiment).HasColumnName("sentiment");
|
||||||
|
builder.Property(r => r.QualityScore).HasColumnName("quality_score");
|
||||||
|
builder.Property(r => r.IsInformative).HasColumnName("is_informative");
|
||||||
|
builder.Property(r => r.LlmTags).HasColumnName("llm_tags");
|
||||||
|
builder.Property(r => r.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
builder.Property(r => r.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasOne(r => r.Lecture)
|
||||||
|
.WithMany(l => l.Reviews)
|
||||||
|
.HasForeignKey(r => r.LectureId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(r => r.User)
|
||||||
|
.WithMany(u => u.Reviews)
|
||||||
|
.HasForeignKey(r => r.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(r => new { r.LectureId, r.UserId }).IsUnique();
|
||||||
|
builder.HasIndex(r => r.LlmStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class StudentProfileConfiguration : IEntityTypeConfiguration<StudentProfile>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<StudentProfile> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("student_profiles");
|
||||||
|
|
||||||
|
builder.HasKey(s => s.Id);
|
||||||
|
builder.Property(s => s.Id).HasColumnName("id");
|
||||||
|
builder.Property(s => s.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(s => s.StudentId).HasColumnName("student_id").HasMaxLength(50);
|
||||||
|
builder.Property(s => s.GroupName).HasColumnName("group_name").HasMaxLength(100);
|
||||||
|
builder.Property(s => s.EnrollmentYear).HasColumnName("enrollment_year");
|
||||||
|
builder.Property(s => s.Faculty).HasColumnName("faculty").HasMaxLength(255);
|
||||||
|
builder.Property(s => s.Specialty).HasColumnName("specialty").HasMaxLength(255);
|
||||||
|
|
||||||
|
builder.HasOne(s => s.User)
|
||||||
|
.WithOne(u => u.StudentProfile)
|
||||||
|
.HasForeignKey<StudentProfile>(s => s.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(s => s.UserId).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class TagConfiguration : IEntityTypeConfiguration<Tag>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Tag> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("tags");
|
||||||
|
|
||||||
|
builder.HasKey(t => t.Id);
|
||||||
|
builder.Property(t => t.Id).HasColumnName("id");
|
||||||
|
builder.Property(t => t.Name).HasColumnName("name").HasMaxLength(255).IsRequired();
|
||||||
|
builder.Property(t => t.Type).HasColumnName("type");
|
||||||
|
builder.Property(t => t.ParentId).HasColumnName("parent_id");
|
||||||
|
builder.Property(t => t.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasOne(t => t.Parent)
|
||||||
|
.WithMany(t => t.Children)
|
||||||
|
.HasForeignKey(t => t.ParentId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class TeacherProfileConfiguration : IEntityTypeConfiguration<TeacherProfile>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<TeacherProfile> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("teacher_profiles");
|
||||||
|
|
||||||
|
builder.HasKey(t => t.Id);
|
||||||
|
builder.Property(t => t.Id).HasColumnName("id");
|
||||||
|
builder.Property(t => t.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(t => t.Department).HasColumnName("department").HasMaxLength(255);
|
||||||
|
builder.Property(t => t.Title).HasColumnName("title").HasMaxLength(255);
|
||||||
|
builder.Property(t => t.Bio).HasColumnName("bio");
|
||||||
|
builder.Property(t => t.ModeusId).HasColumnName("modeus_id").HasMaxLength(255);
|
||||||
|
|
||||||
|
builder.HasOne(t => t.User)
|
||||||
|
.WithOne(u => u.TeacherProfile)
|
||||||
|
.HasForeignKey<TeacherProfile>(t => t.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(t => t.UserId).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class UserAchievementConfiguration : IEntityTypeConfiguration<UserAchievement>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserAchievement> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("user_achievements");
|
||||||
|
|
||||||
|
builder.HasKey(ua => ua.Id);
|
||||||
|
builder.Property(ua => ua.Id).HasColumnName("id");
|
||||||
|
builder.Property(ua => ua.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(ua => ua.AchievementId).HasColumnName("achievement_id");
|
||||||
|
builder.Property(ua => ua.AwardedAt).HasColumnName("awarded_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasOne(ua => ua.User)
|
||||||
|
.WithMany(u => u.UserAchievements)
|
||||||
|
.HasForeignKey(ua => ua.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(ua => ua.Achievement)
|
||||||
|
.WithMany(a => a.UserAchievements)
|
||||||
|
.HasForeignKey(ua => ua.AchievementId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasIndex(ua => new { ua.UserId, ua.AchievementId }).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<User> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("users");
|
||||||
|
|
||||||
|
builder.HasKey(u => u.Id);
|
||||||
|
builder.Property(u => u.Id).HasColumnName("id");
|
||||||
|
builder.Property(u => u.Email).HasColumnName("email").HasMaxLength(255).IsRequired();
|
||||||
|
builder.Property(u => u.DisplayName).HasColumnName("display_name").HasMaxLength(255);
|
||||||
|
builder.Property(u => u.AvatarUrl).HasColumnName("avatar_url").HasMaxLength(500);
|
||||||
|
builder.Property(u => u.Role).HasColumnName("role");
|
||||||
|
builder.Property(u => u.IsActive).HasColumnName("is_active").HasDefaultValue(true);
|
||||||
|
builder.Property(u => u.MicrosoftId).HasColumnName("microsoft_id").HasMaxLength(255);
|
||||||
|
builder.Property(u => u.Xp).HasColumnName("xp").HasDefaultValue(0);
|
||||||
|
builder.Property(u => u.Coins).HasColumnName("coins").HasDefaultValue(0);
|
||||||
|
builder.Property(u => u.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||||
|
builder.Property(u => u.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
builder.HasIndex(u => u.Email).IsUnique();
|
||||||
|
builder.HasIndex(u => u.MicrosoftId).IsUnique().HasFilter("microsoft_id IS NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
+955
@@ -0,0 +1,955 @@
|
|||||||
|
// <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 UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260428124938_Initial")]
|
||||||
|
partial class Initial
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "coin_transaction_type", "coin_transaction_type", new[] { "review_reward", "achievement_reward", "attendance_reward", "admin_adjustment" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "review_llm_status", "review_llm_status", new[] { "pending", "analyzed", "rejected" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "review_sentiment", "review_sentiment", new[] { "positive", "neutral", "negative" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "tag_type", "tag_type", new[] { "institute", "faculty", "subject", "organization", "topic", "other" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_role", "user_role", new[] { "student", "teacher", "admin" });
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CoinReward")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("coin_reward");
|
||||||
|
|
||||||
|
b.Property<string>("Condition")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("condition");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("icon_url");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("XpReward")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("xp_reward");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("achievements", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("AchievementId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("achievement_id");
|
||||||
|
|
||||||
|
b.Property<int>("Amount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("amount");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<int?>("ReviewId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("review_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("ReviewId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("coin_transactions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Course", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSynced")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_synced");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("courses", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CourseTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CourseId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("course_id");
|
||||||
|
|
||||||
|
b.Property<int>("TagId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("tag_id");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("CourseId", "TagId");
|
||||||
|
|
||||||
|
b.HasIndex("TagId");
|
||||||
|
|
||||||
|
b.HasIndex("CourseId", "TagId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("course_tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CourseId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("course_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime>("EndsAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("ends_at");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("format");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOpen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is_open");
|
||||||
|
|
||||||
|
b.Property<int?>("LocationId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("location_id");
|
||||||
|
|
||||||
|
b.Property<int>("MaxEnrollments")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("max_enrollments");
|
||||||
|
|
||||||
|
b.Property<string>("OnlineUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("online_url");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartsAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("starts_at");
|
||||||
|
|
||||||
|
b.Property<int?>("TeacherId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("teacher_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CourseId");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.HasIndex("LocationId");
|
||||||
|
|
||||||
|
b.HasIndex("StartsAt");
|
||||||
|
|
||||||
|
b.HasIndex("TeacherId");
|
||||||
|
|
||||||
|
b.ToTable("lectures", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LectureEnrollment", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("LectureId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("lecture_id");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<bool>("Attended")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("attended");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("LectureId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("LectureId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("lecture_enrollments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("address");
|
||||||
|
|
||||||
|
b.Property<string>("Building")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("building");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Room")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("room");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("locations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires_at");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked_at");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("token");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("refresh_tokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsInformative")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_informative");
|
||||||
|
|
||||||
|
b.Property<int>("LectureId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("lecture_id");
|
||||||
|
|
||||||
|
b.Property<int>("LlmStatus")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("llm_status");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("LlmTags")
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("llm_tags");
|
||||||
|
|
||||||
|
b.Property<double?>("QualityScore")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("quality_score");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("rating");
|
||||||
|
|
||||||
|
b.Property<int?>("Sentiment")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("sentiment");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LlmStatus");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("LectureId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("reviews", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.StudentProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("EnrollmentYear")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("enrollment_year");
|
||||||
|
|
||||||
|
b.Property<string>("Faculty")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("faculty");
|
||||||
|
|
||||||
|
b.Property<string>("GroupName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("group_name");
|
||||||
|
|
||||||
|
b.Property<string>("Specialty")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("specialty");
|
||||||
|
|
||||||
|
b.Property<string>("StudentId")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("student_id");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("student_profiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("parent_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.TeacherProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bio");
|
||||||
|
|
||||||
|
b.Property<string>("Department")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("department");
|
||||||
|
|
||||||
|
b.Property<string>("ModeusId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("modeus_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("teacher_profiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("avatar_url");
|
||||||
|
|
||||||
|
b.Property<int>("Coins")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("coins");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("display_name");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is_active");
|
||||||
|
|
||||||
|
b.Property<string>("MicrosoftId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("microsoft_id");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Xp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("xp");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("MicrosoftId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("microsoft_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserAchievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("AchievementId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("achievement_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AwardedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("awarded_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "AchievementId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("user_achievements", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AchievementId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Review", "Review")
|
||||||
|
.WithMany("CoinTransactions")
|
||||||
|
.HasForeignKey("ReviewId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("CoinTransactions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Achievement");
|
||||||
|
|
||||||
|
b.Navigation("Review");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CourseTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Course", "Course")
|
||||||
|
.WithMany("CourseTags")
|
||||||
|
.HasForeignKey("CourseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Tag", "Tag")
|
||||||
|
.WithMany("CourseTags")
|
||||||
|
.HasForeignKey("TagId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Course");
|
||||||
|
|
||||||
|
b.Navigation("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Course", "Course")
|
||||||
|
.WithMany("Lectures")
|
||||||
|
.HasForeignKey("CourseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Location", "Location")
|
||||||
|
.WithMany("Lectures")
|
||||||
|
.HasForeignKey("LocationId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "Teacher")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TeacherId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Course");
|
||||||
|
|
||||||
|
b.Navigation("Location");
|
||||||
|
|
||||||
|
b.Navigation("Teacher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LectureEnrollment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Lecture", "Lecture")
|
||||||
|
.WithMany("Enrollments")
|
||||||
|
.HasForeignKey("LectureId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Enrollments")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Lecture");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Lecture", "Lecture")
|
||||||
|
.WithMany("Reviews")
|
||||||
|
.HasForeignKey("LectureId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Reviews")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Lecture");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.StudentProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithOne("StudentProfile")
|
||||||
|
.HasForeignKey("UniVerse.Domain.Entities.StudentProfile", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Tag", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.TeacherProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithOne("TeacherProfile")
|
||||||
|
.HasForeignKey("UniVerse.Domain.Entities.TeacherProfile", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserAchievement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
|
.WithMany("UserAchievements")
|
||||||
|
.HasForeignKey("AchievementId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("UserAchievements")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Achievement");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserAchievements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Course", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CourseTags");
|
||||||
|
|
||||||
|
b.Navigation("Lectures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Enrollments");
|
||||||
|
|
||||||
|
b.Navigation("Reviews");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Lectures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CoinTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("CourseTags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CoinTransactions");
|
||||||
|
|
||||||
|
b.Navigation("Enrollments");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Reviews");
|
||||||
|
|
||||||
|
b.Navigation("StudentProfile");
|
||||||
|
|
||||||
|
b.Navigation("TeacherProfile");
|
||||||
|
|
||||||
|
b.Navigation("UserAchievements");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:Enum:coin_transaction_type.coin_transaction_type", "review_reward,achievement_reward,attendance_reward,admin_adjustment")
|
||||||
|
.Annotation("Npgsql:Enum:review_llm_status.review_llm_status", "pending,analyzed,rejected")
|
||||||
|
.Annotation("Npgsql:Enum:review_sentiment.review_sentiment", "positive,neutral,negative")
|
||||||
|
.Annotation("Npgsql:Enum:tag_type.tag_type", "institute,faculty,subject,organization,topic,other")
|
||||||
|
.Annotation("Npgsql:Enum:user_role.user_role", "student,teacher,admin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "achievements",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
description = table.Column<string>(type: "text", nullable: true),
|
||||||
|
icon_url = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
xp_reward = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
coin_reward = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
condition = table.Column<string>(type: "text", nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_achievements", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "courses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
name = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||||
|
description = table.Column<string>(type: "text", nullable: true),
|
||||||
|
external_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
is_synced = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"),
|
||||||
|
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_courses", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "locations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
building = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
room = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
address = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
external_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_locations", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "tags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
parent_id = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_tags", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_tags_tags_parent_id",
|
||||||
|
column: x => x.parent_id,
|
||||||
|
principalTable: "tags",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
email = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
|
display_name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
avatar_url = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
role = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
is_active = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||||
|
microsoft_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
xp = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
coins = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"),
|
||||||
|
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_users", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "course_tags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
course_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
tag_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_course_tags", x => new { x.course_id, x.tag_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_course_tags_courses_course_id",
|
||||||
|
column: x => x.course_id,
|
||||||
|
principalTable: "courses",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_course_tags_tags_tag_id",
|
||||||
|
column: x => x.tag_id,
|
||||||
|
principalTable: "tags",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lectures",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
course_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
teacher_id = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
location_id = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
title = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||||
|
description = table.Column<string>(type: "text", nullable: true),
|
||||||
|
format = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
starts_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
ends_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
is_open = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||||
|
max_enrollments = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
external_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
online_url = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"),
|
||||||
|
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_lectures", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_lectures_courses_course_id",
|
||||||
|
column: x => x.course_id,
|
||||||
|
principalTable: "courses",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_lectures_locations_location_id",
|
||||||
|
column: x => x.location_id,
|
||||||
|
principalTable: "locations",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_lectures_users_teacher_id",
|
||||||
|
column: x => x.teacher_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "refresh_tokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
token = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||||
|
expires_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"),
|
||||||
|
revoked_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_refresh_tokens", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_refresh_tokens_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "student_profiles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
student_id = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||||
|
group_name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
enrollment_year = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
faculty = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
specialty = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_student_profiles", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_student_profiles_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "teacher_profiles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
department = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
title = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
bio = table.Column<string>(type: "text", nullable: true),
|
||||||
|
modeus_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_teacher_profiles", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_teacher_profiles_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_achievements",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
achievement_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
awarded_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_user_achievements", x => new { x.user_id, x.achievement_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_user_achievements_achievements_achievement_id",
|
||||||
|
column: x => x.achievement_id,
|
||||||
|
principalTable: "achievements",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_user_achievements_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lecture_enrollments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
lecture_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
attended = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_lecture_enrollments", x => new { x.lecture_id, x.user_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_lecture_enrollments_lectures_lecture_id",
|
||||||
|
column: x => x.lecture_id,
|
||||||
|
principalTable: "lectures",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_lecture_enrollments_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "reviews",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
lecture_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
rating = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
text = table.Column<string>(type: "text", nullable: true),
|
||||||
|
llm_status = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
sentiment = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
quality_score = table.Column<double>(type: "double precision", nullable: true),
|
||||||
|
is_informative = table.Column<bool>(type: "boolean", nullable: true),
|
||||||
|
llm_tags = table.Column<string[]>(type: "text[]", nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"),
|
||||||
|
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_reviews", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_reviews_lectures_lecture_id",
|
||||||
|
column: x => x.lecture_id,
|
||||||
|
principalTable: "lectures",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_reviews_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "coin_transactions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
amount = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
review_id = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
achievement_id = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_coin_transactions", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_coin_transactions_achievements_achievement_id",
|
||||||
|
column: x => x.achievement_id,
|
||||||
|
principalTable: "achievements",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_coin_transactions_reviews_review_id",
|
||||||
|
column: x => x.review_id,
|
||||||
|
principalTable: "reviews",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_coin_transactions_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_coin_transactions_achievement_id",
|
||||||
|
table: "coin_transactions",
|
||||||
|
column: "achievement_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_coin_transactions_review_id",
|
||||||
|
table: "coin_transactions",
|
||||||
|
column: "review_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_coin_transactions_user_id",
|
||||||
|
table: "coin_transactions",
|
||||||
|
column: "user_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_course_tags_course_id_tag_id",
|
||||||
|
table: "course_tags",
|
||||||
|
columns: new[] { "course_id", "tag_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_course_tags_tag_id",
|
||||||
|
table: "course_tags",
|
||||||
|
column: "tag_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_courses_external_id",
|
||||||
|
table: "courses",
|
||||||
|
column: "external_id",
|
||||||
|
unique: true,
|
||||||
|
filter: "external_id IS NOT NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lecture_enrollments_lecture_id_user_id",
|
||||||
|
table: "lecture_enrollments",
|
||||||
|
columns: new[] { "lecture_id", "user_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lecture_enrollments_user_id",
|
||||||
|
table: "lecture_enrollments",
|
||||||
|
column: "user_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lectures_course_id",
|
||||||
|
table: "lectures",
|
||||||
|
column: "course_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lectures_external_id",
|
||||||
|
table: "lectures",
|
||||||
|
column: "external_id",
|
||||||
|
unique: true,
|
||||||
|
filter: "external_id IS NOT NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lectures_location_id",
|
||||||
|
table: "lectures",
|
||||||
|
column: "location_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lectures_starts_at",
|
||||||
|
table: "lectures",
|
||||||
|
column: "starts_at");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_lectures_teacher_id",
|
||||||
|
table: "lectures",
|
||||||
|
column: "teacher_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_locations_external_id",
|
||||||
|
table: "locations",
|
||||||
|
column: "external_id",
|
||||||
|
unique: true,
|
||||||
|
filter: "external_id IS NOT NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_refresh_tokens_token",
|
||||||
|
table: "refresh_tokens",
|
||||||
|
column: "token",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_refresh_tokens_user_id",
|
||||||
|
table: "refresh_tokens",
|
||||||
|
column: "user_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_reviews_lecture_id_user_id",
|
||||||
|
table: "reviews",
|
||||||
|
columns: new[] { "lecture_id", "user_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_reviews_llm_status",
|
||||||
|
table: "reviews",
|
||||||
|
column: "llm_status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_reviews_user_id",
|
||||||
|
table: "reviews",
|
||||||
|
column: "user_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_student_profiles_user_id",
|
||||||
|
table: "student_profiles",
|
||||||
|
column: "user_id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_tags_parent_id",
|
||||||
|
table: "tags",
|
||||||
|
column: "parent_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_teacher_profiles_user_id",
|
||||||
|
table: "teacher_profiles",
|
||||||
|
column: "user_id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_user_achievements_achievement_id",
|
||||||
|
table: "user_achievements",
|
||||||
|
column: "achievement_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_user_achievements_user_id_achievement_id",
|
||||||
|
table: "user_achievements",
|
||||||
|
columns: new[] { "user_id", "achievement_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_users_email",
|
||||||
|
table: "users",
|
||||||
|
column: "email",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_users_microsoft_id",
|
||||||
|
table: "users",
|
||||||
|
column: "microsoft_id",
|
||||||
|
unique: true,
|
||||||
|
filter: "microsoft_id IS NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "coin_transactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "course_tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lecture_enrollments");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "refresh_tokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "student_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "teacher_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_achievements");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "reviews");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "achievements");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lectures");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "courses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "locations");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,952 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "coin_transaction_type", "coin_transaction_type", new[] { "review_reward", "achievement_reward", "attendance_reward", "admin_adjustment" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "review_llm_status", "review_llm_status", new[] { "pending", "analyzed", "rejected" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "review_sentiment", "review_sentiment", new[] { "positive", "neutral", "negative" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "tag_type", "tag_type", new[] { "institute", "faculty", "subject", "organization", "topic", "other" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_role", "user_role", new[] { "student", "teacher", "admin" });
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CoinReward")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("coin_reward");
|
||||||
|
|
||||||
|
b.Property<string>("Condition")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("condition");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("icon_url");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("XpReward")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("xp_reward");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("achievements", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("AchievementId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("achievement_id");
|
||||||
|
|
||||||
|
b.Property<int>("Amount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("amount");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<int?>("ReviewId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("review_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("ReviewId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("coin_transactions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Course", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSynced")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_synced");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("courses", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CourseTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CourseId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("course_id");
|
||||||
|
|
||||||
|
b.Property<int>("TagId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("tag_id");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("CourseId", "TagId");
|
||||||
|
|
||||||
|
b.HasIndex("TagId");
|
||||||
|
|
||||||
|
b.HasIndex("CourseId", "TagId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("course_tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CourseId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("course_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime>("EndsAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("ends_at");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("format");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOpen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is_open");
|
||||||
|
|
||||||
|
b.Property<int?>("LocationId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("location_id");
|
||||||
|
|
||||||
|
b.Property<int>("MaxEnrollments")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("max_enrollments");
|
||||||
|
|
||||||
|
b.Property<string>("OnlineUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("online_url");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartsAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("starts_at");
|
||||||
|
|
||||||
|
b.Property<int?>("TeacherId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("teacher_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CourseId");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.HasIndex("LocationId");
|
||||||
|
|
||||||
|
b.HasIndex("StartsAt");
|
||||||
|
|
||||||
|
b.HasIndex("TeacherId");
|
||||||
|
|
||||||
|
b.ToTable("lectures", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LectureEnrollment", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("LectureId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("lecture_id");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<bool>("Attended")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("attended");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("LectureId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("LectureId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("lecture_enrollments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("address");
|
||||||
|
|
||||||
|
b.Property<string>("Building")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("building");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Room")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("room");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("locations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires_at");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked_at");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("token");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("refresh_tokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsInformative")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_informative");
|
||||||
|
|
||||||
|
b.Property<int>("LectureId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("lecture_id");
|
||||||
|
|
||||||
|
b.Property<int>("LlmStatus")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("llm_status");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("LlmTags")
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("llm_tags");
|
||||||
|
|
||||||
|
b.Property<double?>("QualityScore")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("quality_score");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("rating");
|
||||||
|
|
||||||
|
b.Property<int?>("Sentiment")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("sentiment");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LlmStatus");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("LectureId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("reviews", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.StudentProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("EnrollmentYear")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("enrollment_year");
|
||||||
|
|
||||||
|
b.Property<string>("Faculty")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("faculty");
|
||||||
|
|
||||||
|
b.Property<string>("GroupName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("group_name");
|
||||||
|
|
||||||
|
b.Property<string>("Specialty")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("specialty");
|
||||||
|
|
||||||
|
b.Property<string>("StudentId")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("student_id");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("student_profiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("parent_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.TeacherProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bio");
|
||||||
|
|
||||||
|
b.Property<string>("Department")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("department");
|
||||||
|
|
||||||
|
b.Property<string>("ModeusId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("modeus_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("teacher_profiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("avatar_url");
|
||||||
|
|
||||||
|
b.Property<int>("Coins")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("coins");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("display_name");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is_active");
|
||||||
|
|
||||||
|
b.Property<string>("MicrosoftId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("microsoft_id");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Xp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("xp");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("MicrosoftId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("microsoft_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserAchievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("AchievementId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("achievement_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AwardedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("awarded_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "AchievementId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("user_achievements", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AchievementId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Review", "Review")
|
||||||
|
.WithMany("CoinTransactions")
|
||||||
|
.HasForeignKey("ReviewId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("CoinTransactions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Achievement");
|
||||||
|
|
||||||
|
b.Navigation("Review");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CourseTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Course", "Course")
|
||||||
|
.WithMany("CourseTags")
|
||||||
|
.HasForeignKey("CourseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Tag", "Tag")
|
||||||
|
.WithMany("CourseTags")
|
||||||
|
.HasForeignKey("TagId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Course");
|
||||||
|
|
||||||
|
b.Navigation("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Course", "Course")
|
||||||
|
.WithMany("Lectures")
|
||||||
|
.HasForeignKey("CourseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Location", "Location")
|
||||||
|
.WithMany("Lectures")
|
||||||
|
.HasForeignKey("LocationId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "Teacher")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TeacherId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Course");
|
||||||
|
|
||||||
|
b.Navigation("Location");
|
||||||
|
|
||||||
|
b.Navigation("Teacher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LectureEnrollment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Lecture", "Lecture")
|
||||||
|
.WithMany("Enrollments")
|
||||||
|
.HasForeignKey("LectureId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Enrollments")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Lecture");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Lecture", "Lecture")
|
||||||
|
.WithMany("Reviews")
|
||||||
|
.HasForeignKey("LectureId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Reviews")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Lecture");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.StudentProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithOne("StudentProfile")
|
||||||
|
.HasForeignKey("UniVerse.Domain.Entities.StudentProfile", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Tag", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.TeacherProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithOne("TeacherProfile")
|
||||||
|
.HasForeignKey("UniVerse.Domain.Entities.TeacherProfile", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserAchievement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
|
.WithMany("UserAchievements")
|
||||||
|
.HasForeignKey("AchievementId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("UserAchievements")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Achievement");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserAchievements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Course", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CourseTags");
|
||||||
|
|
||||||
|
b.Navigation("Lectures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Enrollments");
|
||||||
|
|
||||||
|
b.Navigation("Reviews");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Lectures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CoinTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("CourseTags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CoinTransactions");
|
||||||
|
|
||||||
|
b.Navigation("Enrollments");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Reviews");
|
||||||
|
|
||||||
|
b.Navigation("StudentProfile");
|
||||||
|
|
||||||
|
b.Navigation("TeacherProfile");
|
||||||
|
|
||||||
|
b.Navigation("UserAchievements");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.ExternalServices;
|
||||||
|
|
||||||
|
public class LlmClient : ILlmClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<LlmClient> _logger;
|
||||||
|
|
||||||
|
public LlmClient(HttpClient http, IConfiguration config, ILogger<LlmClient> logger)
|
||||||
|
{
|
||||||
|
_http = http; _config = config; _logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LlmReviewAnalysis> AnalyzeReviewAsync(string reviewText, string lectureContext)
|
||||||
|
{
|
||||||
|
var prompt = $"""
|
||||||
|
Analyze the following student review of a lecture. Return a JSON object with:
|
||||||
|
- quality_score: float 0-1 indicating review quality
|
||||||
|
- sentiment: "Positive", "Neutral", or "Negative"
|
||||||
|
- tags: array of relevant topic tags
|
||||||
|
- is_informative: boolean indicating if the review is informative
|
||||||
|
|
||||||
|
Lecture context: {lectureContext}
|
||||||
|
Review text: {reviewText}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var request = new
|
||||||
|
{
|
||||||
|
model = _config["Llm:Model"] ?? "gpt-4o-mini",
|
||||||
|
messages = new[] { new { role = "user", content = prompt } },
|
||||||
|
temperature = 0.3,
|
||||||
|
response_format = new { type = "json_object" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var apiKey = _config["Llm:ApiKey"] ?? "";
|
||||||
|
_http.DefaultRequestHeaders.Clear();
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
_http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||||
|
|
||||||
|
var response = await _http.PostAsJsonAsync("chat/completions", request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var content = json.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString()!;
|
||||||
|
var analysis = JsonSerializer.Deserialize<LlmRawResponse>(content,
|
||||||
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||||
|
|
||||||
|
return new LlmReviewAnalysis(analysis.QualityScore, analysis.Sentiment, analysis.Tags, analysis.IsInformative);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LlmRawResponse(double QualityScore, string Sentiment, string[] Tags, bool IsInformative);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using UniVerse.Application.DTOs.Sync;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.ExternalServices;
|
||||||
|
|
||||||
|
public class ModeusApiClient : IModeusApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly ILogger<ModeusApiClient> _logger;
|
||||||
|
|
||||||
|
public ModeusApiClient(HttpClient http, IConfiguration config, ILogger<ModeusApiClient> logger)
|
||||||
|
{
|
||||||
|
_http = http; _logger = logger;
|
||||||
|
var apiKey = config["ModeusApi:ApiKey"];
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
_http.DefaultRequestHeaders.Add("X-API-Key", apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ModeusEventsResponse> SearchEventsAsync(SyncScheduleRequest request)
|
||||||
|
{
|
||||||
|
var body = new { specialtyCode = request.SpecialtyCode, timeMin = request.TimeMin, timeMax = request.TimeMax, typeId = request.TypeId };
|
||||||
|
var response = await _http.PostAsJsonAsync("/api/proxy/events/search", body);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadFromJsonAsync<ModeusEventsResponse>() ?? new(new());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ModeusRoomsResponse> SearchRoomsAsync()
|
||||||
|
{
|
||||||
|
var response = await _http.PostAsJsonAsync("/api/proxy/rooms/search", new { });
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadFromJsonAsync<ModeusRoomsResponse>() ?? new(new());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ModeusEmployee>> SearchEmployeeAsync(string fullname)
|
||||||
|
{
|
||||||
|
var response = await _http.GetFromJsonAsync<List<ModeusEmployee>>(
|
||||||
|
$"/api/schedule/searchemployee?fullname={Uri.EscapeDataString(fullname)}");
|
||||||
|
return response ?? new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Achievements;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class AchievementService : IAchievementService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
public AchievementService(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<List<AchievementDto>> GetAllAsync() =>
|
||||||
|
await _db.Achievements.OrderBy(a => a.Name).Select(a => a.ToDto()).ToListAsync();
|
||||||
|
|
||||||
|
public async Task<AchievementDto> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var a = await _db.Achievements.FindAsync(id) ?? throw new NotFoundException("Achievement", id);
|
||||||
|
return a.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AchievementDto> CreateAsync(CreateAchievementRequest req)
|
||||||
|
{
|
||||||
|
var a = new Achievement
|
||||||
|
{
|
||||||
|
Name = req.Name, Description = req.Description, IconUrl = req.IconUrl,
|
||||||
|
XpReward = req.XpReward, CoinReward = req.CoinReward, Condition = req.Condition
|
||||||
|
};
|
||||||
|
_db.Achievements.Add(a);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return a.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AchievementDto> UpdateAsync(int id, UpdateAchievementRequest req)
|
||||||
|
{
|
||||||
|
var a = await _db.Achievements.FindAsync(id) ?? throw new NotFoundException("Achievement", id);
|
||||||
|
a.Name = req.Name; a.Description = req.Description; a.IconUrl = req.IconUrl;
|
||||||
|
a.XpReward = req.XpReward; a.CoinReward = req.CoinReward; a.Condition = req.Condition;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return a.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var a = await _db.Achievements.FindAsync(id) ?? throw new NotFoundException("Achievement", id);
|
||||||
|
_db.Achievements.Remove(a);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using UniVerse.Application.DTOs.Auth;
|
||||||
|
using UniVerse.Application.DTOs.Users;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly IGamificationService _gamification;
|
||||||
|
|
||||||
|
public AuthService(AppDbContext db, IConfiguration config, IGamificationService gamification)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_config = config;
|
||||||
|
_gamification = gamification;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AuthResponse> LoginWithMicrosoftAsync(string authorizationCode)
|
||||||
|
{
|
||||||
|
// Stub: in production, exchange authorizationCode with Microsoft Entra ID
|
||||||
|
// For now, create/find a demo user
|
||||||
|
throw new NotImplementedException(
|
||||||
|
"Microsoft Entra ID integration not yet configured. Use /api/v1/auth/login/dev in Development mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AuthResponse> DevLoginAsync(string email, string? displayName, UserRole role)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
user = new User
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
DisplayName = displayName ?? email.Split('@')[0],
|
||||||
|
Role = role,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
_db.Users.Add(user);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Create profile based on role
|
||||||
|
if (role == UserRole.Student)
|
||||||
|
{
|
||||||
|
_db.StudentProfiles.Add(new StudentProfile { UserId = user.Id });
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
else if (role == UserRole.Teacher)
|
||||||
|
{
|
||||||
|
_db.TeacherProfiles.Add(new TeacherProfile { UserId = user.Id });
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.IsActive)
|
||||||
|
throw new ForbiddenException("Account is deactivated.");
|
||||||
|
|
||||||
|
var accessToken = GenerateAccessToken(user);
|
||||||
|
var refreshToken = await GenerateRefreshTokenAsync(user.Id);
|
||||||
|
|
||||||
|
return new AuthResponse(
|
||||||
|
accessToken,
|
||||||
|
DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
|
||||||
|
user.ToAuthDto()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AuthResponse> RefreshTokenAsync(string refreshToken)
|
||||||
|
{
|
||||||
|
var token = await _db.RefreshTokens
|
||||||
|
.Include(rt => rt.User)
|
||||||
|
.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||||
|
|
||||||
|
if (token == null || !token.IsActive)
|
||||||
|
throw new ForbiddenException("Invalid or expired refresh token.");
|
||||||
|
|
||||||
|
// Revoke old token
|
||||||
|
token.RevokedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Generate new tokens
|
||||||
|
var accessToken = GenerateAccessToken(token.User);
|
||||||
|
var newRefreshToken = await GenerateRefreshTokenAsync(token.UserId);
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new AuthResponse(
|
||||||
|
accessToken,
|
||||||
|
DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
|
||||||
|
token.User.ToAuthDto()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevokeRefreshTokenAsync(string refreshToken)
|
||||||
|
{
|
||||||
|
var token = await _db.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||||
|
if (token != null && token.IsActive)
|
||||||
|
{
|
||||||
|
token.RevokedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDto> GetCurrentUserAsync(int userId)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(userId)
|
||||||
|
?? throw new NotFoundException("User", userId);
|
||||||
|
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateAccessToken(User user)
|
||||||
|
{
|
||||||
|
var key = new SymmetricSecurityKey(
|
||||||
|
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||||
|
new Claim(ClaimTypes.Role, user.Role.ToString()),
|
||||||
|
new Claim("display_name", user.DisplayName ?? "")
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: _config["Jwt:Issuer"],
|
||||||
|
audience: _config["Jwt:Audience"],
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateRefreshTokenAsync(int userId)
|
||||||
|
{
|
||||||
|
var randomBytes = RandomNumberGenerator.GetBytes(64);
|
||||||
|
var tokenString = Convert.ToBase64String(randomBytes);
|
||||||
|
|
||||||
|
var refreshToken = new RefreshToken
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Token = tokenString,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(GetRefreshTokenExpiration()),
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.RefreshTokens.Add(refreshToken);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return tokenString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetAccessTokenExpiration() =>
|
||||||
|
int.Parse(_config["Jwt:AccessTokenExpirationMinutes"] ?? "30");
|
||||||
|
|
||||||
|
private int GetRefreshTokenExpiration() =>
|
||||||
|
int.Parse(_config["Jwt:RefreshTokenExpirationDays"] ?? "30");
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Courses;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class CourseService : ICourseService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
public CourseService(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<PagedResult<CourseDto>> GetAllAsync(CourseFilterRequest filter)
|
||||||
|
{
|
||||||
|
var query = _db.Courses.Include(c => c.CourseTags).ThenInclude(ct => ct.Tag).AsQueryable();
|
||||||
|
if (!string.IsNullOrEmpty(filter.Search))
|
||||||
|
query = query.Where(c => c.Name.ToLower().Contains(filter.Search.ToLower()));
|
||||||
|
if (filter.IsSynced.HasValue)
|
||||||
|
query = query.Where(c => c.IsSynced == filter.IsSynced.Value);
|
||||||
|
if (filter.TagId.HasValue)
|
||||||
|
query = query.Where(c => c.CourseTags.Any(ct => ct.TagId == filter.TagId.Value));
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var courses = await query.OrderByDescending(c => c.CreatedAt)
|
||||||
|
.Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
|
||||||
|
return PagedResult<CourseDto>.Create(courses.Select(c => c.ToDto()).ToList(), total, filter.Page, filter.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CourseDto> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var course = await _db.Courses.Include(c => c.CourseTags).ThenInclude(ct => ct.Tag)
|
||||||
|
.FirstOrDefaultAsync(c => c.Id == id) ?? throw new NotFoundException("Course", id);
|
||||||
|
return course.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CourseDto> CreateAsync(CreateCourseRequest request)
|
||||||
|
{
|
||||||
|
var course = new Course { Name = request.Name, Description = request.Description };
|
||||||
|
_db.Courses.Add(course);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return await GetByIdAsync(course.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CourseDto> UpdateAsync(int id, UpdateCourseRequest request)
|
||||||
|
{
|
||||||
|
var course = await _db.Courses.FindAsync(id) ?? throw new NotFoundException("Course", id);
|
||||||
|
course.Name = request.Name;
|
||||||
|
course.Description = request.Description;
|
||||||
|
course.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return await GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var course = await _db.Courses.FindAsync(id) ?? throw new NotFoundException("Course", id);
|
||||||
|
_db.Courses.Remove(course);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddTagAsync(int courseId, int tagId)
|
||||||
|
{
|
||||||
|
if (await _db.CourseTags.AnyAsync(ct => ct.CourseId == courseId && ct.TagId == tagId))
|
||||||
|
throw new ConflictException("Tag already linked to course.");
|
||||||
|
_ = await _db.Courses.FindAsync(courseId) ?? throw new NotFoundException("Course", courseId);
|
||||||
|
_ = await _db.Tags.FindAsync(tagId) ?? throw new NotFoundException("Tag", tagId);
|
||||||
|
_db.CourseTags.Add(new CourseTag { CourseId = courseId, TagId = tagId });
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveTagAsync(int courseId, int tagId)
|
||||||
|
{
|
||||||
|
var ct = await _db.CourseTags.FirstOrDefaultAsync(x => x.CourseId == courseId && x.TagId == tagId)
|
||||||
|
?? throw new NotFoundException("CourseTag not found.");
|
||||||
|
_db.CourseTags.Remove(ct);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using UniVerse.Application.DTOs.Achievements;
|
||||||
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Gamification;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class GamificationService : IGamificationService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<GamificationService> _logger;
|
||||||
|
|
||||||
|
public GamificationService(AppDbContext db, IConfiguration config, ILogger<GamificationService> logger)
|
||||||
|
{
|
||||||
|
_db = db; _config = config; _logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
|
||||||
|
int? reviewId = null, int? achievementId = null, string? description = null)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(userId);
|
||||||
|
if (user == null) return;
|
||||||
|
user.Coins += amount;
|
||||||
|
user.Xp += amount;
|
||||||
|
_db.CoinTransactions.Add(new CoinTransaction
|
||||||
|
{
|
||||||
|
UserId = userId, Amount = amount, Type = type,
|
||||||
|
ReviewId = reviewId, AchievementId = achievementId, Description = description
|
||||||
|
});
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Awarded {Amount} coins to user {UserId} ({Type})", amount, userId, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CheckAndAwardAchievementsAsync(int userId)
|
||||||
|
{
|
||||||
|
var achievements = await _db.Achievements.ToListAsync();
|
||||||
|
var existing = await _db.UserAchievements.Where(ua => ua.UserId == userId)
|
||||||
|
.Select(ua => ua.AchievementId).ToListAsync();
|
||||||
|
var reviews = await _db.Reviews.CountAsync(r => r.UserId == userId);
|
||||||
|
var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId && e.Attended);
|
||||||
|
|
||||||
|
foreach (var achievement in achievements.Where(a => !existing.Contains(a.Id)))
|
||||||
|
{
|
||||||
|
var earned = achievement.Condition switch
|
||||||
|
{
|
||||||
|
"reviews_1" => reviews >= 1,
|
||||||
|
"reviews_5" => reviews >= 5,
|
||||||
|
"reviews_10" => reviews >= 10,
|
||||||
|
"attended_5" => attended >= 5,
|
||||||
|
"attended_10" => attended >= 10,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
if (!earned) continue;
|
||||||
|
_db.UserAchievements.Add(new UserAchievement { UserId = userId, AchievementId = achievement.Id });
|
||||||
|
if (achievement.CoinReward > 0)
|
||||||
|
await AwardCoinsAsync(userId, achievement.CoinReward, CoinTransactionType.AchievementReward,
|
||||||
|
achievementId: achievement.Id, description: $"Achievement: {achievement.Name}");
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CalculateLevel(int xp)
|
||||||
|
{
|
||||||
|
var thresholds = _config.GetSection("Gamification:XpThresholds").Get<int[]>()
|
||||||
|
?? [0, 100, 300, 600, 1000, 1500, 2500, 4000];
|
||||||
|
for (int i = thresholds.Length - 1; i >= 0; i--)
|
||||||
|
if (xp >= thresholds[i]) return i + 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId) =>
|
||||||
|
await _db.UserAchievements.Include(ua => ua.Achievement)
|
||||||
|
.Where(ua => ua.UserId == userId).OrderByDescending(ua => ua.AwardedAt)
|
||||||
|
.Select(ua => ua.ToDto()).ToListAsync();
|
||||||
|
|
||||||
|
public async Task<PagedResult<CoinTransactionDto>> GetTransactionsAsync(int userId, PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
var query = _db.CoinTransactions.Where(ct => ct.UserId == userId);
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query.OrderByDescending(ct => ct.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize)
|
||||||
|
.Select(ct => ct.ToDto()).ToListAsync();
|
||||||
|
return PagedResult<CoinTransactionDto>.Create(items, total, pagination.Page, pagination.PageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Lectures;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class LectureService : ILectureService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
public LectureService(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
private IQueryable<Lecture> BaseQuery() => _db.Lectures
|
||||||
|
.Include(l => l.Course).Include(l => l.Teacher)
|
||||||
|
.Include(l => l.Location).Include(l => l.Enrollments);
|
||||||
|
|
||||||
|
public async Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter)
|
||||||
|
{
|
||||||
|
var query = BaseQuery();
|
||||||
|
if (filter.CourseId.HasValue) query = query.Where(l => l.CourseId == filter.CourseId);
|
||||||
|
if (filter.TeacherId.HasValue) query = query.Where(l => l.TeacherId == filter.TeacherId);
|
||||||
|
if (filter.Format.HasValue) query = query.Where(l => l.Format == filter.Format);
|
||||||
|
if (filter.IsOpen.HasValue) query = query.Where(l => l.IsOpen == filter.IsOpen);
|
||||||
|
if (filter.DateFrom.HasValue)
|
||||||
|
query = query.Where(l => DateOnly.FromDateTime(l.StartsAt) >= filter.DateFrom);
|
||||||
|
if (filter.DateTo.HasValue)
|
||||||
|
query = query.Where(l => DateOnly.FromDateTime(l.StartsAt) <= filter.DateTo);
|
||||||
|
if (!string.IsNullOrEmpty(filter.Search))
|
||||||
|
query = query.Where(l => l.Title.ToLower().Contains(filter.Search.ToLower()));
|
||||||
|
if (filter.TagId.HasValue)
|
||||||
|
query = query.Where(l => l.Course.CourseTags.Any(ct => ct.TagId == filter.TagId));
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query.OrderBy(l => l.StartsAt)
|
||||||
|
.Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
|
||||||
|
return PagedResult<LectureDto>.Create(items.Select(l => l.ToDto()).ToList(), total, filter.Page, filter.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null)
|
||||||
|
{
|
||||||
|
var lecture = await BaseQuery().FirstOrDefaultAsync(l => l.Id == id)
|
||||||
|
?? throw new NotFoundException("Lecture", id);
|
||||||
|
var isEnrolled = currentUserId.HasValue &&
|
||||||
|
lecture.Enrollments.Any(e => e.UserId == currentUserId.Value);
|
||||||
|
return lecture.ToDetailDto(isEnrolled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LectureDto> CreateAsync(CreateLectureRequest req)
|
||||||
|
{
|
||||||
|
_ = await _db.Courses.FindAsync(req.CourseId) ?? throw new NotFoundException("Course", req.CourseId);
|
||||||
|
var lecture = new Lecture
|
||||||
|
{
|
||||||
|
CourseId = req.CourseId, TeacherId = req.TeacherId, LocationId = req.LocationId,
|
||||||
|
Title = req.Title, Description = req.Description, Format = req.Format,
|
||||||
|
StartsAt = req.StartsAt, EndsAt = req.EndsAt, IsOpen = req.IsOpen,
|
||||||
|
MaxEnrollments = req.MaxEnrollments, OnlineUrl = req.OnlineUrl
|
||||||
|
};
|
||||||
|
_db.Lectures.Add(lecture);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
var full = await BaseQuery().FirstAsync(l => l.Id == lecture.Id);
|
||||||
|
return full.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest req)
|
||||||
|
{
|
||||||
|
var lecture = await _db.Lectures.FindAsync(id) ?? throw new NotFoundException("Lecture", id);
|
||||||
|
lecture.TeacherId = req.TeacherId; lecture.LocationId = req.LocationId;
|
||||||
|
lecture.Title = req.Title; lecture.Description = req.Description;
|
||||||
|
lecture.Format = req.Format; lecture.StartsAt = req.StartsAt; lecture.EndsAt = req.EndsAt;
|
||||||
|
lecture.IsOpen = req.IsOpen; lecture.MaxEnrollments = req.MaxEnrollments;
|
||||||
|
lecture.OnlineUrl = req.OnlineUrl; lecture.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
var full = await BaseQuery().FirstAsync(l => l.Id == id);
|
||||||
|
return full.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var lecture = await _db.Lectures.FindAsync(id) ?? throw new NotFoundException("Lecture", id);
|
||||||
|
_db.Lectures.Remove(lecture);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnrollAsync(int lectureId, int userId)
|
||||||
|
{
|
||||||
|
var lecture = await _db.Lectures.Include(l => l.Enrollments)
|
||||||
|
.FirstOrDefaultAsync(l => l.Id == lectureId) ?? throw new NotFoundException("Lecture", lectureId);
|
||||||
|
if (!lecture.IsOpen) throw new ConflictException("Lecture is not open for enrollment.");
|
||||||
|
if (lecture.MaxEnrollments > 0 && lecture.Enrollments.Count >= lecture.MaxEnrollments)
|
||||||
|
throw new ConflictException("Lecture is full.");
|
||||||
|
if (lecture.Enrollments.Any(e => e.UserId == userId))
|
||||||
|
throw new ConflictException("Already enrolled.");
|
||||||
|
_db.LectureEnrollments.Add(new LectureEnrollment { LectureId = lectureId, UserId = userId });
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnenrollAsync(int lectureId, int userId)
|
||||||
|
{
|
||||||
|
var enrollment = await _db.LectureEnrollments
|
||||||
|
.FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
|
||||||
|
?? throw new NotFoundException("Enrollment not found.");
|
||||||
|
_db.LectureEnrollments.Remove(enrollment);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MarkAttendanceAsync(int lectureId, int userId, bool attended)
|
||||||
|
{
|
||||||
|
var enrollment = await _db.LectureEnrollments
|
||||||
|
.FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
|
||||||
|
?? throw new NotFoundException("Enrollment not found.");
|
||||||
|
enrollment.Attended = attended;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<EnrollmentDto>> GetEnrollmentsAsync(int lectureId, PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
var query = _db.LectureEnrollments.Include(e => e.User)
|
||||||
|
.Where(e => e.LectureId == lectureId);
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query.OrderBy(e => e.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||||
|
return PagedResult<EnrollmentDto>.Create(items.Select(e => e.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class LlmAnalysisService : ILlmAnalysisService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly ILlmClient _llm;
|
||||||
|
private readonly IGamificationService _gamification;
|
||||||
|
private readonly ILogger<LlmAnalysisService> _logger;
|
||||||
|
|
||||||
|
public LlmAnalysisService(AppDbContext db, ILlmClient llm,
|
||||||
|
IGamificationService gamification, ILogger<LlmAnalysisService> logger)
|
||||||
|
{
|
||||||
|
_db = db; _llm = llm; _gamification = gamification; _logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AnalyzeReviewAsync(int reviewId)
|
||||||
|
{
|
||||||
|
var review = await _db.Reviews.Include(r => r.Lecture)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == reviewId);
|
||||||
|
if (review == null || review.LlmStatus != ReviewLlmStatus.Pending) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = $"Lecture: {review.Lecture?.Title}";
|
||||||
|
var result = await _llm.AnalyzeReviewAsync(review.Text ?? "", context);
|
||||||
|
|
||||||
|
review.QualityScore = result.QualityScore;
|
||||||
|
review.Sentiment = Enum.TryParse<ReviewSentiment>(result.Sentiment, true, out var s)
|
||||||
|
? s : ReviewSentiment.Neutral;
|
||||||
|
review.LlmTags = result.Tags;
|
||||||
|
review.IsInformative = result.IsInformative;
|
||||||
|
review.LlmStatus = ReviewLlmStatus.Analyzed;
|
||||||
|
review.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (result.IsInformative)
|
||||||
|
await _gamification.AwardCoinsAsync(review.UserId, 10,
|
||||||
|
CoinTransactionType.ReviewReward, reviewId: review.Id,
|
||||||
|
description: "Informative review reward");
|
||||||
|
|
||||||
|
await _gamification.CheckAndAwardAchievementsAsync(review.UserId);
|
||||||
|
_logger.LogInformation("Review {ReviewId} analyzed successfully", reviewId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to analyze review {ReviewId}, will retry later", reviewId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ProcessPendingReviewsAsync()
|
||||||
|
{
|
||||||
|
var pending = await _db.Reviews
|
||||||
|
.Where(r => r.LlmStatus == ReviewLlmStatus.Pending)
|
||||||
|
.OrderBy(r => r.CreatedAt).Take(10)
|
||||||
|
.Select(r => r.Id).ToListAsync();
|
||||||
|
|
||||||
|
foreach (var id in pending)
|
||||||
|
await AnalyzeReviewAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Locations;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class LocationService : ILocationService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public LocationService(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<List<LocationDto>> GetAllAsync() =>
|
||||||
|
await _db.Locations.OrderBy(l => l.Name)
|
||||||
|
.Select(l => new LocationDto(l.Id, l.Name, l.Building, l.Room, l.Address, l.CreatedAt))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task<LocationDto> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var loc = await _db.Locations.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("Location", id);
|
||||||
|
return loc.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LocationDto> CreateAsync(CreateLocationRequest request)
|
||||||
|
{
|
||||||
|
var loc = new Location
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
Building = request.Building,
|
||||||
|
Room = request.Room,
|
||||||
|
Address = request.Address
|
||||||
|
};
|
||||||
|
_db.Locations.Add(loc);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return loc.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LocationDto> UpdateAsync(int id, UpdateLocationRequest request)
|
||||||
|
{
|
||||||
|
var loc = await _db.Locations.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("Location", id);
|
||||||
|
|
||||||
|
loc.Name = request.Name;
|
||||||
|
loc.Building = request.Building;
|
||||||
|
loc.Room = request.Room;
|
||||||
|
loc.Address = request.Address;
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return loc.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var loc = await _db.Locations.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("Location", id);
|
||||||
|
_db.Locations.Remove(loc);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Reviews;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class ReviewService : IReviewService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
public ReviewService(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
private IQueryable<Review> BaseQuery() => _db.Reviews
|
||||||
|
.Include(r => r.Lecture).Include(r => r.User);
|
||||||
|
|
||||||
|
public async Task<ReviewDto> CreateAsync(int userId, CreateReviewRequest req)
|
||||||
|
{
|
||||||
|
_ = await _db.Lectures.FindAsync(req.LectureId) ?? throw new NotFoundException("Lecture", req.LectureId);
|
||||||
|
if (await _db.Reviews.AnyAsync(r => r.LectureId == req.LectureId && r.UserId == userId))
|
||||||
|
throw new ConflictException("You already reviewed this lecture.");
|
||||||
|
var review = new Review
|
||||||
|
{
|
||||||
|
LectureId = req.LectureId, UserId = userId,
|
||||||
|
Rating = req.Rating, Text = req.Text,
|
||||||
|
LlmStatus = ReviewLlmStatus.Pending
|
||||||
|
};
|
||||||
|
_db.Reviews.Add(review);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
var full = await BaseQuery().FirstAsync(r => r.Id == review.Id);
|
||||||
|
return full.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ReviewDto> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var review = await BaseQuery().FirstOrDefaultAsync(r => r.Id == id)
|
||||||
|
?? throw new NotFoundException("Review", id);
|
||||||
|
return review.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ReviewDto> UpdateAsync(int id, int userId, UpdateReviewRequest req)
|
||||||
|
{
|
||||||
|
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||||
|
if (review.UserId != userId) throw new ForbiddenException();
|
||||||
|
review.Rating = req.Rating; review.Text = req.Text;
|
||||||
|
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||||
|
review.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return await GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id, int userId, bool isAdmin = false)
|
||||||
|
{
|
||||||
|
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||||
|
if (review.UserId != userId && !isAdmin) throw new ForbiddenException();
|
||||||
|
_db.Reviews.Remove(review);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<ReviewDto>> GetByLectureAsync(int lectureId, PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
var query = BaseQuery().Where(r => r.LectureId == lectureId);
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||||
|
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<ReviewDto>> GetByUserAsync(int userId, PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
var query = BaseQuery().Where(r => r.UserId == userId);
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||||
|
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<ReviewDto>> GetPendingAsync(PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
var query = BaseQuery().Where(r => r.LlmStatus == ReviewLlmStatus.Pending);
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query.OrderBy(r => r.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||||
|
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReanalyzeAsync(int id)
|
||||||
|
{
|
||||||
|
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||||
|
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||||
|
review.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using UniVerse.Application.DTOs.Sync;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class ScheduleSyncService : IScheduleSyncService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly IModeusApiClient _modeus;
|
||||||
|
private readonly ILogger<ScheduleSyncService> _logger;
|
||||||
|
private static SyncStatusDto _lastStatus = new(null, "idle", null);
|
||||||
|
|
||||||
|
public ScheduleSyncService(AppDbContext db, IModeusApiClient modeus, ILogger<ScheduleSyncService> logger)
|
||||||
|
{
|
||||||
|
_db = db; _modeus = modeus; _logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncResultDto> SyncScheduleAsync(SyncScheduleRequest request)
|
||||||
|
{
|
||||||
|
int created = 0, updated = 0, skipped = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var events = await _modeus.SearchEventsAsync(request);
|
||||||
|
foreach (var ev in events.Events)
|
||||||
|
{
|
||||||
|
var existing = await _db.Lectures.FirstOrDefaultAsync(l => l.ExternalId == ev.Id);
|
||||||
|
if (existing != null) { updated++; existing.StartsAt = ev.StartsAt; existing.EndsAt = ev.EndsAt; existing.UpdatedAt = DateTime.UtcNow; }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var course = await _db.Courses.FirstOrDefaultAsync(c => c.ExternalId == ev.TypeId);
|
||||||
|
if (course == null) { course = new Course { Name = ev.Name, ExternalId = ev.TypeId, IsSynced = true }; _db.Courses.Add(course); await _db.SaveChangesAsync(); }
|
||||||
|
_db.Lectures.Add(new Lecture { CourseId = course.Id, Title = ev.Name, ExternalId = ev.Id, StartsAt = ev.StartsAt, EndsAt = ev.EndsAt });
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
var result = new SyncResultDto(created, updated, skipped, null);
|
||||||
|
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "completed", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Schedule sync failed");
|
||||||
|
var result = new SyncResultDto(created, updated, skipped, ex.Message);
|
||||||
|
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncResultDto> SyncRoomsAsync()
|
||||||
|
{
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
var rooms = await _modeus.SearchRoomsAsync();
|
||||||
|
foreach (var room in rooms.Rooms)
|
||||||
|
{
|
||||||
|
var existing = await _db.Locations.FirstOrDefaultAsync(l => l.ExternalId == room.Id);
|
||||||
|
if (existing != null) { existing.Name = room.Name; existing.Building = room.Building; updated++; }
|
||||||
|
else { _db.Locations.Add(new Location { Name = room.Name, Building = room.Building, ExternalId = room.Id }); created++; }
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return new SyncResultDto(created, updated, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<EmployeeDto>> SearchEmployeesAsync(string fullname)
|
||||||
|
{
|
||||||
|
var employees = await _modeus.SearchEmployeeAsync(fullname);
|
||||||
|
return employees.Select(e => new EmployeeDto(e.Id, e.FullName, e.Department)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Tags;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class TagService : ITagService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public TagService(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<List<TagDto>> GetAllAsync(TagType? type = null, int? parentId = null)
|
||||||
|
{
|
||||||
|
var query = _db.Tags.AsQueryable();
|
||||||
|
if (type.HasValue) query = query.Where(t => t.Type == type.Value);
|
||||||
|
if (parentId.HasValue) query = query.Where(t => t.ParentId == parentId.Value);
|
||||||
|
|
||||||
|
return await query.OrderBy(t => t.Name)
|
||||||
|
.Select(t => new TagDto(t.Id, t.Name, t.Type, t.ParentId, t.CreatedAt))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagDto> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var tag = await _db.Tags.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("Tag", id);
|
||||||
|
return tag.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagDto> CreateAsync(CreateTagRequest request)
|
||||||
|
{
|
||||||
|
if (request.ParentId.HasValue)
|
||||||
|
{
|
||||||
|
var parent = await _db.Tags.FindAsync(request.ParentId.Value)
|
||||||
|
?? throw new NotFoundException("Parent Tag", request.ParentId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag = new Tag
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
Type = request.Type,
|
||||||
|
ParentId = request.ParentId
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Tags.Add(tag);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return tag.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagDto> UpdateAsync(int id, UpdateTagRequest request)
|
||||||
|
{
|
||||||
|
var tag = await _db.Tags.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("Tag", id);
|
||||||
|
|
||||||
|
tag.Name = request.Name;
|
||||||
|
tag.Type = request.Type;
|
||||||
|
tag.ParentId = request.ParentId;
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return tag.ToDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var tag = await _db.Tags.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("Tag", id);
|
||||||
|
_db.Tags.Remove(tag);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TagTreeDto>> GetTreeAsync()
|
||||||
|
{
|
||||||
|
var allTags = await _db.Tags
|
||||||
|
.Include(t => t.Children)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var roots = allTags.Where(t => t.ParentId == null).ToList();
|
||||||
|
return roots.Select(t => BuildTree(t, allTags)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TagTreeDto BuildTree(Tag tag, List<Tag> allTags)
|
||||||
|
{
|
||||||
|
var children = allTags.Where(t => t.ParentId == tag.Id).ToList();
|
||||||
|
return new TagTreeDto(
|
||||||
|
tag.Id, tag.Name, tag.Type,
|
||||||
|
children.Select(c => BuildTree(c, allTags)).ToList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Users;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Application.Mappings;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
using UniVerse.Domain.Exceptions;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class UserService : IUserService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly IGamificationService _gamification;
|
||||||
|
|
||||||
|
public UserService(AppDbContext db, IGamificationService gamification)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_gamification = gamification;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDto> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("User", id);
|
||||||
|
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("User", id);
|
||||||
|
|
||||||
|
if (request.DisplayName != null) user.DisplayName = request.DisplayName;
|
||||||
|
if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserStatsDto> GetStatsAsync(int id)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("User", id);
|
||||||
|
|
||||||
|
var totalLectures = await _db.LectureEnrollments.CountAsync(e => e.UserId == id);
|
||||||
|
var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == id && e.Attended);
|
||||||
|
var reviews = await _db.Reviews.CountAsync(r => r.UserId == id);
|
||||||
|
var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id);
|
||||||
|
|
||||||
|
return new UserStatsDto(
|
||||||
|
totalLectures, attended, reviews,
|
||||||
|
user.Xp, user.Coins, _gamification.CalculateLevel(user.Xp), achievements
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter)
|
||||||
|
{
|
||||||
|
var query = _db.Users.AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(filter.Search))
|
||||||
|
{
|
||||||
|
var search = filter.Search.ToLower();
|
||||||
|
query = query.Where(u =>
|
||||||
|
u.Email.ToLower().Contains(search) ||
|
||||||
|
(u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Role.HasValue)
|
||||||
|
query = query.Where(u => u.Role == filter.Role.Value);
|
||||||
|
|
||||||
|
if (filter.IsActive.HasValue)
|
||||||
|
query = query.Where(u => u.IsActive == filter.IsActive.Value);
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
|
var users = await query
|
||||||
|
.OrderByDescending(u => u.CreatedAt)
|
||||||
|
.Skip((filter.Page - 1) * filter.PageSize)
|
||||||
|
.Take(filter.PageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var items = users.Select(u => u.ToDto(_gamification.CalculateLevel(u.Xp))).ToList();
|
||||||
|
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetRoleAsync(int id, UserRole role)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("User", id);
|
||||||
|
user.Role = role;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetActiveAsync(int id, bool isActive)
|
||||||
|
{
|
||||||
|
var user = await _db.Users.FindAsync(id)
|
||||||
|
?? throw new NotFoundException("User", id);
|
||||||
|
user.IsActive = isActive;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>UniVerse.Infrastructure</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\UniVerse.Domain\UniVerse.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\UniVerse.Application\UniVerse.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user