Got the sliders extracted to their own page

This commit is contained in:
2026-03-08 18:36:49 -04:00
parent dde9ffcedb
commit 28d90fcc18
375 changed files with 2646 additions and 1035 deletions

View File

@@ -33,7 +33,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
.ThenInclude(e => e.ListedNames)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Defects)
.Include(g => g.ViewCount) // Added
.Include(g => g.ViewCount)
.Include(g => g.IdentifierFields)
.Where(g => g.Id == id)
.FirstOrDefaultAsync();
@@ -56,7 +56,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
.ThenInclude(e => e.ListedNames)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Defects)
.Include(g => g.ViewCount) // Added
.Include(g => g.ViewCount)
.Where(g => g.ArtifactGroupingIdentifier == artifactGroupingIdentifier)
.FirstOrDefaultAsync();
}
@@ -65,25 +65,18 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
{
await using var context = await _context.CreateDbContextAsync();
// Attach the Category to the context. If it has a key, it will be tracked.
context.Attach(grouping.Category);
// --- Local caches for de-duplication within this transaction ---
var processedTypes = new Dictionary<string, ArtifactType>();
var processedLocations = new Dictionary<string, ArtifactStorageLocation>();
var processedTags = new Dictionary<string, ArtifactEntryTag>();
var processedNames = new Dictionary<string, ListedName>();
var processedDefects = new Dictionary<string, ArtifactDefect>();
// --- End local caches ---
// --- Helper functions to get unique entities (from cache or DB) ---
async Task<ArtifactType> GetUniqueTypeAsync(ArtifactType typeToProcess)
async Task<ArtifactType?> GetUniqueTypeAsync(ArtifactType? typeToProcess)
{
if (string.IsNullOrEmpty(typeToProcess?.Name)) return typeToProcess;
if (processedTypes.TryGetValue(typeToProcess.Name, out var uniqueType))
{
return uniqueType;
}
if (typeToProcess == null || string.IsNullOrEmpty(typeToProcess.Name)) return typeToProcess;
if (processedTypes.TryGetValue(typeToProcess.Name, out var uniqueType)) return uniqueType;
var dbType = await context.ArtifactTypes.FirstOrDefaultAsync(t => t.Name == typeToProcess.Name);
if (dbType != null)
{
@@ -94,13 +87,10 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
return typeToProcess;
}
async Task<ArtifactStorageLocation> GetUniqueLocationAsync(ArtifactStorageLocation locationToProcess)
async Task<ArtifactStorageLocation?> GetUniqueLocationAsync(ArtifactStorageLocation? locationToProcess)
{
if (string.IsNullOrEmpty(locationToProcess?.Location)) return locationToProcess;
if (processedLocations.TryGetValue(locationToProcess.Location, out var uniqueLocation))
{
return uniqueLocation;
}
if (locationToProcess == null || string.IsNullOrEmpty(locationToProcess.Location)) return null;
if (processedLocations.TryGetValue(locationToProcess.Location, out var uniqueLocation)) return uniqueLocation;
var dbLocation = await context.ArtifactStorageLocations.FirstOrDefaultAsync(l => l.Location == locationToProcess.Location);
if (dbLocation != null)
{
@@ -114,10 +104,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
async Task<ArtifactEntryTag> GetUniqueTagAsync(ArtifactEntryTag tagToProcess)
{
if (string.IsNullOrEmpty(tagToProcess?.Name)) return tagToProcess;
if (processedTags.TryGetValue(tagToProcess.Name, out var uniqueTag))
{
return uniqueTag;
}
if (processedTags.TryGetValue(tagToProcess.Name, out var uniqueTag)) return uniqueTag;
var dbTag = await context.ArtifactEntryTags.FirstOrDefaultAsync(t => t.Name == tagToProcess.Name);
if (dbTag != null)
{
@@ -131,10 +118,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
async Task<ListedName> GetUniqueNameAsync(ListedName nameToProcess)
{
if (string.IsNullOrEmpty(nameToProcess?.Value)) return nameToProcess;
if (processedNames.TryGetValue(nameToProcess.Value, out var uniqueName))
{
return uniqueName;
}
if (processedNames.TryGetValue(nameToProcess.Value, out var uniqueName)) return uniqueName;
var dbName = await context.ArtifactAssociatedNames.FirstOrDefaultAsync(n => n.Value == nameToProcess.Value);
if (dbName != null)
{
@@ -148,10 +132,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
async Task<ArtifactDefect> GetUniqueDefectAsync(ArtifactDefect defectToProcess)
{
if (string.IsNullOrEmpty(defectToProcess?.Description)) return defectToProcess;
if (processedDefects.TryGetValue(defectToProcess.Description, out var uniqueDefect))
{
return uniqueDefect;
}
if (processedDefects.TryGetValue(defectToProcess.Description, out var uniqueDefect)) return uniqueDefect;
var dbDefect = await context.ArtifactDefects.FirstOrDefaultAsync(d => d.Description == defectToProcess.Description);
if (dbDefect != null)
{
@@ -161,61 +142,38 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
processedDefects[defectToProcess.Description] = defectToProcess;
return defectToProcess;
}
// --- End helper functions ---
// De-duplicate the main grouping's type
grouping.Type = await GetUniqueTypeAsync(grouping.Type);
// Iterate through all child entries to handle their related entities.
foreach (var entry in grouping.ChildArtifactEntries)
{
// Handle Artifact Types
entry.Type = await GetUniqueTypeAsync(entry.Type);
// Handle Storage Location
entry.StorageLocation = await GetUniqueLocationAsync(entry.StorageLocation);
// Handle Tags
var managedTags = new List<ArtifactEntryTag>();
foreach (var tag in entry.Tags)
{
managedTags.Add(await GetUniqueTagAsync(tag));
}
entry.Tags = managedTags;
// Handle Listed Names
var managedNames = new List<ListedName>();
foreach (var name in entry.ListedNames)
{
managedNames.Add(await GetUniqueNameAsync(name));
}
entry.ListedNames = managedNames;
// Handle Defects
var managedDefects = new List<ArtifactDefect>();
foreach (var defect in entry.Defects)
{
managedDefects.Add(await GetUniqueDefectAsync(defect));
}
entry.Defects = managedDefects;
}
// Concatinate all of the text to be searchable by postgres
grouping.GenerateSearchIndex();
// Add the new grouping and save changes.
context.ChangeTracker.TrackGraph(grouping, node =>
{
// If the entity's key is set, EF should treat it as an existing, unchanged entity.
if (node.Entry.IsKeySet)
{
node.Entry.State = EntityState.Unchanged;
}
// Otherwise, it's a new entity that needs to be inserted.
else
{
node.Entry.State = EntityState.Added;
}
});
await context.SaveChangesAsync();
@@ -225,13 +183,11 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
{
await using var context = await _context.CreateDbContextAsync();
// 1. Retrieve the existing grouping object from the database, eagerly loading all related data.
var existingGrouping = await context.ArtifactGroupings
.Include(g => g.Category)
.Include(g => g.IdentifierFields)
.Include(g => g.Type)
.Include(g => g.ViewCount) // Load ViewCount
//.Include(g => g.BlogPosts) // BlogPosts not handled yet
.Include(g => g.ViewCount)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.StorageLocation)
.Include(g => g.ChildArtifactEntries)
@@ -245,27 +201,15 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
.Where(g => g.Id == updatedGrouping.Id)
.FirstOrDefaultAsync();
if (existingGrouping == null)
{
return;
}
if (existingGrouping == null) return;
// 2. Manually copy over primitive properties.
existingGrouping.Title = updatedGrouping.Title;
existingGrouping.Description = updatedGrouping.Description;
existingGrouping.IsPublicallyVisible = updatedGrouping.IsPublicallyVisible;
existingGrouping.IdentifierFields = updatedGrouping.IdentifierFields;
// Handle one-to-many relationships (Type, Category).
var existingGroupingType = await context.ArtifactTypes.FirstOrDefaultAsync(t => t.Name == updatedGrouping.Type.Name);
if (existingGroupingType != null)
{
existingGrouping.Type = existingGroupingType;
}
else
{
existingGrouping.Type = updatedGrouping.Type;
}
existingGrouping.Type = existingGroupingType ?? updatedGrouping.Type;
if (existingGrouping.Category.Name != updatedGrouping.Category.Name)
{
@@ -273,44 +217,32 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
context.Add(existingGrouping.Category);
}
// Handle ViewCount (Added)
if (updatedGrouping.ViewCount != null)
{
if (existingGrouping.ViewCount == null)
{
// Create a new ViewCount
existingGrouping.ViewCount = new ArtifactGroupingViewCount
{
Grouping = existingGrouping,
Grouping = existingGrouping,
Views = updatedGrouping.ViewCount.Views
};
}
else
{
// Update existing ViewCount
existingGrouping.ViewCount.Views = updatedGrouping.ViewCount.Views;
}
}
// TODO: Handle BlogPosts update (requires model definition & de-duplication)
// await DeDuplicateGroupingRelationsAsync(context, updatedGrouping);
// existingGrouping.BlogPosts.Clear();
// updatedGrouping.BlogPosts.ForEach(post => existingGrouping.BlogPosts.Add(post));
// 3. Synchronize the ChildArtifactEntries collection.
var updatedEntryIds = updatedGrouping.ChildArtifactEntries.Select(e => e.Id).ToList();
var entriesToRemove = existingGrouping.ChildArtifactEntries
.Where(e => !updatedEntryIds.Contains(e.Id))
.ToList();
foreach (var entryToRemove in entriesToRemove)
{
existingGrouping.ChildArtifactEntries.Remove(entryToRemove);
}
foreach (var updatedEntry in updatedGrouping.ChildArtifactEntries)
{
// FIRST, de-duplicate all related entities on the incoming entry.
await DeDuplicateEntryRelationsAsync(context, updatedEntry);
var existingEntry = existingGrouping.ChildArtifactEntries
@@ -318,7 +250,6 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
if (existingEntry != null)
{
// The entry exists, so manually update its properties.
existingEntry.Title = updatedEntry.Title;
existingEntry.Description = updatedEntry.Description;
existingEntry.ArtifactNumber = updatedEntry.ArtifactNumber;
@@ -327,12 +258,9 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
existingEntry.FileTextContent = updatedEntry.FileTextContent;
existingEntry.Quantity = updatedEntry.Quantity;
existingEntry.Links = updatedEntry.Links;
// The relations on updatedEntry are already de-duplicated, so just assign them.
existingEntry.StorageLocation = updatedEntry.StorageLocation;
existingEntry.Type = updatedEntry.Type;
// For collections, clear the old ones and add the new de-duplicated ones.
existingEntry.Tags.Clear();
updatedEntry.Tags.ForEach(tag => existingEntry.Tags.Add(tag));
@@ -344,34 +272,37 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
}
else
{
// The entry is new and its children are already de-duplicated, so just add it.
existingGrouping.ChildArtifactEntries.Add(updatedEntry);
}
}
existingGrouping.GenerateSearchIndex();
// 4. Save all changes.
await context.SaveChangesAsync();
}
private async Task DeDuplicateEntryRelationsAsync(ApplicationDbContext context, ArtifactEntry entry)
{
// --- Handle One-to-Many Relationships ---
var existingLocation = await context.ArtifactStorageLocations.FirstOrDefaultAsync(l => l.Location == entry.StorageLocation.Location);
if (existingLocation != null)
// Handle StorageLocation - null if empty
if (entry.StorageLocation != null && !string.IsNullOrEmpty(entry.StorageLocation.Location))
{
entry.StorageLocation = existingLocation;
var existingLocation = await context.ArtifactStorageLocations
.FirstOrDefaultAsync(l => l.Location == entry.StorageLocation.Location);
entry.StorageLocation = existingLocation ?? entry.StorageLocation;
}
else
{
entry.StorageLocation = null;
}
var existingType = await context.ArtifactTypes.FirstOrDefaultAsync(t => t.Name == entry.Type.Name);
if (existingType != null)
// Handle Type - null guard
if (entry.Type != null && !string.IsNullOrEmpty(entry.Type.Name))
{
entry.Type = existingType;
var existingType = await context.ArtifactTypes
.FirstOrDefaultAsync(t => t.Name == entry.Type.Name);
entry.Type = existingType ?? entry.Type;
}
// --- Handle Many-to-Many Relationships ---
// De-duplicate Tags
var processedTags = new List<ArtifactEntryTag>();
foreach (var tag in entry.Tags)
@@ -406,34 +337,23 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
}
}
/// <summary>
/// A helper method to synchronize many-to-many collections.
/// </summary>
private async Task SyncCollectionAsync<TEntity, TKey>(
DbContext context,
ICollection<TEntity> existingItems,
ICollection<TEntity> updatedItems,
Func<TEntity, TKey> keySelector) where TEntity : class
DbContext context,
ICollection<TEntity> existingItems,
ICollection<TEntity> updatedItems,
Func<TEntity, TKey> keySelector) where TEntity : class
{
var existingKeys = existingItems.Select(keySelector).ToHashSet();
var updatedKeys = updatedItems.Select(keySelector).ToHashSet();
// 1. Remove items that are no longer in the updated collection
var keysToRemove = existingKeys.Except(updatedKeys);
var itemsToRemove = existingItems.Where(item => keysToRemove.Contains(keySelector(item))).ToList();
foreach (var item in itemsToRemove)
{
existingItems.Remove(item);
}
// 2. Identify keys for brand new items
var keysToAdd = updatedKeys.Except(existingKeys).ToList();
if (!keysToAdd.Any())
{
return; // Nothing to add
}
if (!keysToAdd.Any()) return;
// 3. Batch-fetch all entities from the DB that match the new keys.
Dictionary<TKey, TEntity> existingDbItemsMap = [];
if (typeof(TEntity) == typeof(ArtifactEntryTag))
{
@@ -459,23 +379,14 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
.ToListAsync();
existingDbItemsMap = defects.ToDictionary(d => (TKey)(object)d.Description) as Dictionary<TKey, TEntity>;
}
// TODO: Add support for other entity types like BlogPost or ArtifactEntry if needed
// 4. Add the items, using the tracked entity from the DB if it exists.
foreach (var updatedItem in updatedItems.Where(i => keysToAdd.Contains(keySelector(i))))
{
var key = keySelector(updatedItem);
if (existingDbItemsMap.TryGetValue(key, out var dbItem))
{
// The item already exists in the DB, so add the tracked version.
existingItems.Add(dbItem);
}
else
{
// This is a brand new item, so add the untracked one from the input.
existingItems.Add(updatedItem);
}
}
}
@@ -504,8 +415,6 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
throw new ArgumentOutOfRangeException($"Either page number or number of results was less than or equal to 0. {nameof(pageNumber)}={pageNumber} {nameof(resultsCount)}={resultsCount}");
}
var totalCount = await context.ArtifactGroupings.CountAsync();
var items = await context.ArtifactGroupings
.Include(g => g.ChildArtifactEntries)
.Include(g => g.Category)

View File

@@ -5,47 +5,54 @@ namespace OpenArchival.DataAccess;
public class FilePathListingProvider : IFilePathListingProvider
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
private readonly ILogger<FilePathListingProvider> _logger;
[SetsRequiredMembers]
public FilePathListingProvider(ApplicationDbContext context, ILogger<FilePathListingProvider> logger)
public FilePathListingProvider(IDbContextFactory<ApplicationDbContext> factory, ILogger<FilePathListingProvider> logger)
{
_context = context;
_contextFactory = factory;
_logger = logger;
}
public async Task<FilePathListing?> GetFilePathListingAsync(int id)
{
return await _context.ArtifactFilePaths.Where(f => f.Id == id).FirstOrDefaultAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
return await context.ArtifactFilePaths.Where(f => f.Id == id).FirstOrDefaultAsync();
}
public async Task<FilePathListing?> GetFilePathListingByPathAsync(string path)
{
return await _context.ArtifactFilePaths.Where(f => f.Path == path).FirstOrDefaultAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
return await context.ArtifactFilePaths.Where(f => f.Path == path).FirstOrDefaultAsync();
}
public async Task CreateFilePathListingAsync(FilePathListing filePathListing)
{
_context.ArtifactFilePaths.Add(filePathListing);
await _context.SaveChangesAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
context.ArtifactFilePaths.Add(filePathListing);
await context.SaveChangesAsync();
}
public async Task UpdateFilePathListingAsync(FilePathListing filePathListing)
{
_context.ArtifactFilePaths.Update(filePathListing);
await _context.SaveChangesAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
context.ArtifactFilePaths.Update(filePathListing);
await context.SaveChangesAsync();
}
public async Task DeleteFilePathListingAsync(FilePathListing filePathListing)
{
_context.ArtifactFilePaths.Remove(filePathListing);
await _context.SaveChangesAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
context.ArtifactFilePaths.Remove(filePathListing);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteFilePathListingAsync(string originalFileName, string diskPath)
{
var listingToDelete = await _context.ArtifactFilePaths
await using var context = await _contextFactory.CreateDbContextAsync();
var listingToDelete = await context.ArtifactFilePaths
.Where(p => p.OriginalName == originalFileName)
.Where(p => p.Path == diskPath)
.FirstOrDefaultAsync();
@@ -55,7 +62,8 @@ public class FilePathListingProvider : IFilePathListingProvider
return false;
}
_context.RemoveRange(listingToDelete);
context.RemoveRange(listingToDelete);
await context.SaveChangesAsync();
return true;
}
}

View File

@@ -15,7 +15,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("OpenArchival.DataAccess")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+dde9ffcedb0cf584318d02205327e3d89d7f3dfb")]
[assembly: System.Reflection.AssemblyProductAttribute("OpenArchival.DataAccess")]
[assembly: System.Reflection.AssemblyTitleAttribute("OpenArchival.DataAccess")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
c8c747ee3374803ead7300c9cde5e52ee95acfe8a814f315c634208d734612ae
dc52d04cc3c44ec1e1f7c68c59e1efcc9564b7d37139fb78e19ce06e69441da6

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"xv45WNT2g2XkogtgnZ44UpFRBjZ2iZdpriOCF04ZK+0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Ap1LS2Bu\u002BlbeHcQc7/mIjvf\u002BPMTy7htRuShcfgTbCrA=","/\u002BUMDoi/qJv4E27Z1Kdi5OA8EElZtZDVp1UNzs3Q57Y=","AZ2OSNlSeBWOQOKxwZ4sozly3K8\u002BI0/3eD0YApiQVCw=","pMd54fdzP1kbqafTlUzXcRB4MPEHWTF0FW0zgCl6j\u002BQ=","Ey\u002BBcQmzo7EhVqWBQCTcsoZaHh\u002BYYMImfO5Q1sFq\u002BIE="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"xv45WNT2g2XkogtgnZ44UpFRBjZ2iZdpriOCF04ZK+0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Ap1LS2Bu\u002BlbeHcQc7/mIjvf\u002BPMTy7htRuShcfgTbCrA=","/\u002BUMDoi/qJv4E27Z1Kdi5OA8EElZtZDVp1UNzs3Q57Y=","AZ2OSNlSeBWOQOKxwZ4sozly3K8\u002BI0/3eD0YApiQVCw=","pMd54fdzP1kbqafTlUzXcRB4MPEHWTF0FW0zgCl6j\u002BQ=","6r9KZkIwYsT3Ht846lxPtYo7gb0MkP1lhyqOsADXq6U="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"DYy3s2sbfV/4UGwrtlTKRWjMCNJZIQmzu5Royx6M/LQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Ap1LS2Bu\u002BlbeHcQc7/mIjvf\u002BPMTy7htRuShcfgTbCrA=","/\u002BUMDoi/qJv4E27Z1Kdi5OA8EElZtZDVp1UNzs3Q57Y=","AZ2OSNlSeBWOQOKxwZ4sozly3K8\u002BI0/3eD0YApiQVCw=","pMd54fdzP1kbqafTlUzXcRB4MPEHWTF0FW0zgCl6j\u002BQ=","Ey\u002BBcQmzo7EhVqWBQCTcsoZaHh\u002BYYMImfO5Q1sFq\u002BIE="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"DYy3s2sbfV/4UGwrtlTKRWjMCNJZIQmzu5Royx6M/LQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Ap1LS2Bu\u002BlbeHcQc7/mIjvf\u002BPMTy7htRuShcfgTbCrA=","/\u002BUMDoi/qJv4E27Z1Kdi5OA8EElZtZDVp1UNzs3Q57Y=","AZ2OSNlSeBWOQOKxwZ4sozly3K8\u002BI0/3eD0YApiQVCw=","pMd54fdzP1kbqafTlUzXcRB4MPEHWTF0FW0zgCl6j\u002BQ=","6r9KZkIwYsT3Ht846lxPtYo7gb0MkP1lhyqOsADXq6U="],"CachedAssets":{},"CachedCopyCandidates":{}}