diff --git a/CaPPMS/Data/DBOperationsService.cs b/CaPPMS/Data/DBOperationsService.cs index ea81c2b..7297ecf 100644 --- a/CaPPMS/Data/DBOperationsService.cs +++ b/CaPPMS/Data/DBOperationsService.cs @@ -4,6 +4,7 @@ using Humanizer; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; +using Octokit; using System; using System.Collections.Generic; using System.Data; @@ -21,12 +22,10 @@ public class DBOperationsService private const string ConnectionStringFormat = @"Data Source={0}"; private const string RetriveStudentScoreDetailsFileName = "ReadStudentScoreDetails.sql"; private const string ReadStudentScoresFileName = "ReadStudentScores.sql"; - private const string ReadStudentScoreByStudentFileName = "ReadStudentScoreByStudent.sql"; private const string StudentDataBaseCreation = "StudentDataBaseCreation.sql"; - + private const string TableNameNotFoundMessage = "Table name not found for type {0}. Skipping verification."; private static string readStudentScoreDetails; private static string readStudentScore; - private static string readStudentScoreById; private static volatile int dbBroker; private static readonly TimeSpan brokerTimeout = TimeSpan.FromSeconds(10); @@ -35,7 +34,6 @@ static DBOperationsService() { readStudentScoreDetails = GetResourceData(RetriveStudentScoreDetailsFileName); readStudentScore = GetResourceData(ReadStudentScoresFileName); - readStudentScoreById = GetResourceData(ReadStudentScoreByStudentFileName); } private string connectionString; @@ -416,6 +414,10 @@ public async Task EnsureDbExistsAsync() } } + // Verify database. + // Can be that the database is created but the backing tables have changed. + await VerifiyDatabase(); + dbBroker = 0; } @@ -532,5 +534,112 @@ private async Task CreateDbAsync() this.logger.LogError($"Error creating database. Error:{ex.GetBaseException()}"); } } + + private async Task VerifiyDatabase() + { + Type[] dbTypes = [.. Assembly.GetExecutingAssembly() + .GetTypes() + .Where(type => type.GetCustomAttribute() != null)]; + + foreach (Type type in dbTypes) + { + string tableName = ReflectionHelper.GetTableName(type); + if (string.IsNullOrEmpty(tableName)) + { + logger.LogWarning(TableNameNotFoundMessage, type.Name); + continue; + } + + bool result = await VerifyColumnsExistAsync(type, tableName); + if (result) + { + logger.LogDebug($"Table '{tableName}' verification succeeded for type '{type.Name}'."); + } + else + { + logger.LogWarning($"Table '{tableName}' verification failed for type '{type.Name}'."); + } + } + } + + private async Task VerifyColumnsExistAsync(Type type, string tableName) + { + var properties = ReflectionHelper.GetNonIgnoredProperties(type); + var columnNames = await GetColumnNamesFromDatabaseAsync(tableName); + + foreach (var property in properties) + { + if (!columnNames.Contains(property.Name)) + { + logger.LogWarning($"Column '{property.Name}' does not exist in table '{tableName}'. Attempting to add it."); + bool columnAdded = await TryAddColumnAsync(tableName, property); + if (!columnAdded) + { + logger.LogError($"Failed to add column '{property.Name}' to table '{tableName}'."); + return false; + } + } + } + + return true; + } + + private async Task TryAddColumnAsync(string tableName, PropertyInfo property) + { + string columnType = GetSqlType(property.PropertyType); + string query = $"ALTER TABLE {tableName} ADD COLUMN {property.Name} {columnType};"; + + try + { + await ExecuteNonQueryAsync(query); + return true; + } + catch (Exception ex) + { + logger.LogError($"Error adding column '{property.Name}' to table '{tableName}': {ex.Message}"); + return false; + } + } + + private string GetSqlType(Type type) + { + if (type == typeof(int) || type == typeof(long)) + { + return "INTEGER"; + } + if (type == typeof(string) || type == typeof(IEnumerable)) + { + return "TEXT"; + } + if (type == typeof(bool)) + { + return "BOOLEAN"; + } + if (type == typeof(double) || type == typeof(float)) + { + return "REAL"; + } + if (type == typeof(DateTime)) + { + return "DATETIME"; + } + + // Add more type mappings as needed + throw new NotSupportedException($"Type '{type.Name}' is not supported."); + } + + private async Task> GetColumnNamesFromDatabaseAsync(string tableName) + { + var columnNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + var query = $"PRAGMA table_info({tableName})"; + + await this.ExecuteQueryAsync(query, (reader, map) => + { + columnNames.Add(reader.GetString(1)); // Column name is in the second column + }); + + return columnNames; + } } } \ No newline at end of file diff --git a/CaPPMS/Extensions/IDataReaderExtension.cs b/CaPPMS/Extensions/IDataReaderExtension.cs index 019f1fd..996f3c0 100644 --- a/CaPPMS/Extensions/IDataReaderExtension.cs +++ b/CaPPMS/Extensions/IDataReaderExtension.cs @@ -84,6 +84,12 @@ private static object ConvertValue(object value, Type expectedType) return Convert.ToDouble(value); } + // Check booleans + if (expectedType == typeof(bool)) + { + return Convert.ToBoolean(value); + } + // Check lists if (expectedType == typeof(List)) { diff --git a/CaPPMS/Helpers/ReflectionHelper.cs b/CaPPMS/Helpers/ReflectionHelper.cs new file mode 100644 index 0000000..dc9e29f --- /dev/null +++ b/CaPPMS/Helpers/ReflectionHelper.cs @@ -0,0 +1,22 @@ +using CaPPMS.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; + +public static class ReflectionHelper +{ + public static IEnumerable GetNonIgnoredProperties(Type type) + { + return type.GetProperties() + .Where(prop => !Attribute.IsDefined(prop, typeof(IgnoreDataMemberAttribute))); + } + + public static string GetTableName(Type type) + { + var tableNameAttribute = type.GetCustomAttributes(typeof(SqlTableNameAttribute), true) + .FirstOrDefault() as SqlTableNameAttribute; + return tableNameAttribute?.TableName ?? string.Empty; + } +} diff --git a/CaPPMS/Model/Student.cs b/CaPPMS/Model/Student.cs index c04ef88..c75b836 100644 --- a/CaPPMS/Model/Student.cs +++ b/CaPPMS/Model/Student.cs @@ -9,31 +9,55 @@ namespace CaPPMS.Model { + /// + /// Model for the Student. + /// [SqlTableName("Students")] public class Student : ISqlTableModel { private const string EmailSuffix = "@student.umgc.edu"; + /// + /// Initialize a new instance of the class. + /// public Student() { } + /// + /// Gets or sets the student ID. + /// [Required] [SqlIdProperty] public long StudentId { get; set; } = -1; + /// + /// Gets or sets the first name. + /// [ColumnHeader] [DisplayName("First Name")] public string FirstName { get; set; } = string.Empty; + /// + /// Gets or sets the last name. + /// [ColumnHeader] [DisplayName("Last Name")] public string LastName { get; set; } = string.Empty; + /// + /// Gets or sets the email address. + /// [ColumnHeader] public string Email { get; set; } = string.Empty; + /// + /// Gets or sets the GitHub account. + /// [ColumnHeader] public string GitHub { get; set; } = string.Empty; + /// + /// Gets or sets the team name. + /// [ColumnHeader] [DisplayName("Team")] [IgnoreDataMember] @@ -49,9 +73,15 @@ public string? TeamName } } + /// + /// Gets or sets the team assigned to the student. + /// [IgnoreDataMember] public Team AssignedTeam { get; private set; } = new Team(); + /// + /// Gets or sets the team ID. + /// public long TeamId { get @@ -64,8 +94,17 @@ public long TeamId } } + /// + /// Gets or sets the class ID. + /// public long ClassId { get; set; } = -1; + /// + /// Gets or sets a value indicating whether the student is a team leader. + /// + [ColumnHeader] + public bool IsTeamLead { get; set; } = false; + /// /// Set the team for the student. /// diff --git a/CaPPMS/Model/StudentScores.cs b/CaPPMS/Model/StudentScores.cs index a8c2ef6..4c92023 100644 --- a/CaPPMS/Model/StudentScores.cs +++ b/CaPPMS/Model/StudentScores.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using CaPPMS.Attributes; +using System.Collections.Generic; namespace CaPPMS.Model { + [SqlTableName(tableName: "")] public class StudentScores : Student { public StudentScores() { }