Phase 7A Hard Delete
This commit is contained in:
parent
ea3177c2aa
commit
36ae1f95df
@ -21,6 +21,8 @@ public interface IProjectRepository
|
|||||||
Task ArchiveForUserAsync(int projectId, int userId);
|
Task ArchiveForUserAsync(int projectId, int userId);
|
||||||
Task<bool> ArchiveForOwnerAsync(int projectId, int userId);
|
Task<bool> ArchiveForOwnerAsync(int projectId, int userId);
|
||||||
Task<bool> RestoreForOwnerAsync(int projectId, int userId);
|
Task<bool> RestoreForOwnerAsync(int projectId, int userId);
|
||||||
|
Task<IReadOnlyList<string>> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId);
|
||||||
|
Task HardDeleteForOwnerAsync(int projectId, int userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IProjectCollaborationRepository
|
public interface IProjectCollaborationRepository
|
||||||
@ -1290,6 +1292,25 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<string>(
|
||||||
|
"dbo.ProjectHardDelete_ListFilePathsForOwner",
|
||||||
|
new { ProjectID = projectId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.Where(path => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HardDeleteForOwnerAsync(int projectId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"dbo.ProjectHardDelete_ForOwner",
|
||||||
|
new { ProjectID = projectId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ProjectCollaborationRepository(ISqlConnectionFactory connectionFactory) : IProjectCollaborationRepository
|
public sealed class ProjectCollaborationRepository(ISqlConnectionFactory connectionFactory) : IProjectCollaborationRepository
|
||||||
|
|||||||
@ -67,6 +67,47 @@ public sealed class ProjectActivity
|
|||||||
: "System";
|
: "System";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectHardDeleteResult
|
||||||
|
{
|
||||||
|
public bool Succeeded { get; init; }
|
||||||
|
public bool NotFound { get; init; }
|
||||||
|
public bool NotArchived { get; init; }
|
||||||
|
public bool FileCleanupHadFailures { get; init; }
|
||||||
|
public int DeletedFileCount { get; init; }
|
||||||
|
public int MissingFileCount { get; init; }
|
||||||
|
public int FailedFileCount { get; init; }
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public static ProjectHardDeleteResult Success(int deletedFileCount, int missingFileCount, int failedFileCount) => new()
|
||||||
|
{
|
||||||
|
Succeeded = true,
|
||||||
|
FileCleanupHadFailures = failedFileCount > 0,
|
||||||
|
DeletedFileCount = deletedFileCount,
|
||||||
|
MissingFileCount = missingFileCount,
|
||||||
|
FailedFileCount = failedFileCount,
|
||||||
|
Message = failedFileCount > 0
|
||||||
|
? "Project permanently deleted, but some files could not be removed. Check the application logs."
|
||||||
|
: "Project permanently deleted."
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectHardDeleteResult Inaccessible() => new()
|
||||||
|
{
|
||||||
|
NotFound = true,
|
||||||
|
Message = "Project not found."
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectHardDeleteResult ActiveProject() => new()
|
||||||
|
{
|
||||||
|
NotArchived = true,
|
||||||
|
Message = "Only archived projects can be permanently deleted."
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectHardDeleteResult Failure(string message) => new()
|
||||||
|
{
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class UserFile
|
public sealed class UserFile
|
||||||
{
|
{
|
||||||
public long UserFileID { get; set; }
|
public long UserFileID { get; set; }
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public interface IProjectService
|
|||||||
Task<int> SaveAsync(ProjectEditViewModel model);
|
Task<int> SaveAsync(ProjectEditViewModel model);
|
||||||
Task<bool> ArchiveAsync(int projectId);
|
Task<bool> ArchiveAsync(int projectId);
|
||||||
Task<bool> RestoreAsync(int projectId);
|
Task<bool> RestoreAsync(int projectId);
|
||||||
|
Task<ProjectHardDeleteResult> HardDeleteAsync(int projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IProjectCollaborationService
|
public interface IProjectCollaborationService
|
||||||
@ -309,7 +310,13 @@ public interface IAcceptanceService
|
|||||||
Task<Phase1AcceptanceViewModel> GetPhase1ChecklistAsync(int? projectId);
|
Task<Phase1AcceptanceViewModel> GetPhase1ChecklistAsync(int? projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ProjectService(IProjectRepository projects, IBookRepository books, IProjectActivityService activity, ICurrentUserService currentUser) : IProjectService
|
public sealed class ProjectService(
|
||||||
|
IProjectRepository projects,
|
||||||
|
IBookRepository books,
|
||||||
|
IProjectActivityService activity,
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
IWebHostEnvironment environment,
|
||||||
|
ILogger<ProjectService> logger) : IProjectService
|
||||||
{
|
{
|
||||||
public async Task<ProjectIndexViewModel> ListAsync()
|
public async Task<ProjectIndexViewModel> ListAsync()
|
||||||
{
|
{
|
||||||
@ -418,6 +425,158 @@ public sealed class ProjectService(IProjectRepository projects, IBookRepository
|
|||||||
|
|
||||||
return restored;
|
return restored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectHardDeleteResult> HardDeleteAsync(int projectId)
|
||||||
|
{
|
||||||
|
if (!currentUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Hard project delete requested without an authenticated user for project {ProjectID}.", projectId);
|
||||||
|
return ProjectHardDeleteResult.Inaccessible();
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = currentUser.UserId.Value;
|
||||||
|
var project = await projects.GetForUserAsync(projectId, userId);
|
||||||
|
if (project is null || !project.IsOwner)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Hard project delete denied for project {ProjectID} and user {UserID}: project not found or user is not owner.", projectId, userId);
|
||||||
|
return ProjectHardDeleteResult.Inaccessible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!project.IsArchived)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Hard project delete refused for active project {ProjectID} and user {UserID}.", projectId, userId);
|
||||||
|
return ProjectHardDeleteResult.ActiveProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePaths = await projects.ListHardDeleteFilePathsForOwnerAsync(projectId, userId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await projects.HardDeleteForOwnerAsync(projectId, userId);
|
||||||
|
logger.LogInformation("Database hard delete completed for archived project {ProjectID} owned by user {UserID}. {FilePathCount} file paths queued for cleanup.",
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
filePaths.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Database hard delete failed for archived project {ProjectID} owned by user {UserID}. File cleanup was not attempted.",
|
||||||
|
projectId,
|
||||||
|
userId);
|
||||||
|
return ProjectHardDeleteResult.Failure("Project could not be permanently deleted. No files were removed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleanup = DeleteProjectFiles(projectId, filePaths);
|
||||||
|
if (cleanup.Failed > 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Project {ProjectID} was hard deleted from the database but file cleanup had {FailedFileCount} failures, {DeletedFileCount} deleted files, and {MissingFileCount} missing files.",
|
||||||
|
projectId,
|
||||||
|
cleanup.Failed,
|
||||||
|
cleanup.Deleted,
|
||||||
|
cleanup.Missing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("File cleanup completed for hard-deleted project {ProjectID}: {DeletedFileCount} deleted files, {MissingFileCount} missing files.",
|
||||||
|
projectId,
|
||||||
|
cleanup.Deleted,
|
||||||
|
cleanup.Missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectHardDeleteResult.Success(cleanup.Deleted, cleanup.Missing, cleanup.Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int Deleted, int Missing, int Failed) DeleteProjectFiles(int projectId, IReadOnlyList<string> filePaths)
|
||||||
|
{
|
||||||
|
var deleted = 0;
|
||||||
|
var missing = 0;
|
||||||
|
var failed = 0;
|
||||||
|
var deletedDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var filePath in filePaths)
|
||||||
|
{
|
||||||
|
var absolutePath = ResolveProjectUploadPath(filePath);
|
||||||
|
if (absolutePath is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Hard delete skipped a non-project upload path for project {ProjectID}.", projectId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(absolutePath))
|
||||||
|
{
|
||||||
|
missing++;
|
||||||
|
logger.LogInformation("Hard delete file cleanup found a missing file for project {ProjectID}.", projectId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(absolutePath);
|
||||||
|
deleted++;
|
||||||
|
logger.LogInformation("Hard delete removed a project file for project {ProjectID}.", projectId);
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(absolutePath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
deletedDirectories.Add(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failed++;
|
||||||
|
logger.LogWarning(ex, "Hard delete could not remove a project file for project {ProjectID}.", projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var directory in deletedDirectories.OrderByDescending(x => x.Length))
|
||||||
|
{
|
||||||
|
TryDeleteEmptyUploadDirectory(projectId, directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (deleted, missing, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ResolveProjectUploadPath(string storagePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(storagePath)
|
||||||
|
|| storagePath.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| storagePath.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalised = storagePath.Replace('\\', '/').TrimStart('/');
|
||||||
|
if (!normalised.StartsWith("uploads/scenes/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadsRoot = Path.GetFullPath(Path.Combine(environment.WebRootPath, "uploads"));
|
||||||
|
var absolutePath = Path.GetFullPath(Path.Combine(environment.WebRootPath, normalised.Replace('/', Path.DirectorySeparatorChar)));
|
||||||
|
return absolutePath.StartsWith(uploadsRoot, StringComparison.OrdinalIgnoreCase) ? absolutePath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDeleteEmptyUploadDirectory(int projectId, string directory)
|
||||||
|
{
|
||||||
|
var uploadsRoot = Path.GetFullPath(Path.Combine(environment.WebRootPath, "uploads"));
|
||||||
|
var fullDirectory = Path.GetFullPath(directory);
|
||||||
|
if (!fullDirectory.StartsWith(uploadsRoot, StringComparison.OrdinalIgnoreCase) || string.Equals(fullDirectory, uploadsRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(fullDirectory) && !Directory.EnumerateFileSystemEntries(fullDirectory).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(fullDirectory);
|
||||||
|
logger.LogInformation("Hard delete removed an empty upload directory for project {ProjectID}.", projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Hard delete could not remove an empty upload directory for project {ProjectID}.", projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ProjectCollaborationService(
|
public sealed class ProjectCollaborationService(
|
||||||
|
|||||||
347
PlotLine/Sql/054_ProjectHardDeletion.sql
Normal file
347
PlotLine/Sql/054_ProjectHardDeletion.sql
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectHardDelete_ListFilePathsForOwner
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE p.ProjectID = @ProjectID
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.AccessRole = N'Owner'
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
AND p.IsArchived = 1
|
||||||
|
)
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
SELECT DISTINCT StoragePath
|
||||||
|
FROM dbo.UserFile
|
||||||
|
WHERE ProjectID = @ProjectID
|
||||||
|
AND NULLIF(LTRIM(RTRIM(StoragePath)), N'') IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT DISTINCT sa.FilePath
|
||||||
|
FROM dbo.SceneAttachments sa
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID
|
||||||
|
AND NULLIF(LTRIM(RTRIM(sa.FilePath)), N'') IS NOT NULL;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectHardDelete_ForOwner
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE p.ProjectID = @ProjectID
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.AccessRole = N'Owner'
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
)
|
||||||
|
THROW 52001, 'Project was not found for this owner.', 1;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0)
|
||||||
|
THROW 52002, 'Only archived projects can be permanently deleted.', 1;
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
DELETE FROM dbo.ProjectActivity WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE prh
|
||||||
|
FROM dbo.ProjectRestoreHistory prh
|
||||||
|
LEFT JOIN dbo.ProjectRestorePoints sourcePoint ON sourcePoint.ProjectRestorePointID = prh.SourceRestorePointID
|
||||||
|
LEFT JOIN dbo.ProjectRestorePoints safetyPoint ON safetyPoint.ProjectRestorePointID = prh.SafetyRestorePointID
|
||||||
|
WHERE prh.ProjectID = @ProjectID
|
||||||
|
OR sourcePoint.ProjectID = @ProjectID
|
||||||
|
OR safetyPoint.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.ProjectRestorePoints WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.ProjectInvitation WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE uf
|
||||||
|
FROM dbo.UserFile uf
|
||||||
|
WHERE uf.ProjectID = @ProjectID
|
||||||
|
OR EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.SceneAttachments sa
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID
|
||||||
|
AND sa.FilePath = uf.StoragePath
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE sw
|
||||||
|
FROM dbo.ScenarioWarnings sw
|
||||||
|
INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sw.ScenarioID
|
||||||
|
WHERE sc.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE svr
|
||||||
|
FROM dbo.ScenarioValidationRuns svr
|
||||||
|
INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = svr.ScenarioID
|
||||||
|
WHERE sc.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sso
|
||||||
|
FROM dbo.ScenarioSceneOrder sso
|
||||||
|
INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sso.ScenarioID
|
||||||
|
WHERE sc.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.Scenarios WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.ContinuityWarnings WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.ValidationRuns WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sa
|
||||||
|
FROM dbo.SceneAttachments sa
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sci
|
||||||
|
FROM dbo.SceneChecklistItems sci
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sci.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sw
|
||||||
|
FROM dbo.SceneWorkflow sw
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sw.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sn
|
||||||
|
FROM dbo.SceneNotes sn
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE cg
|
||||||
|
FROM dbo.ChapterGoals cg
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = cg.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sd
|
||||||
|
FROM dbo.SceneDependencies sd
|
||||||
|
LEFT JOIN dbo.Scenes ss ON ss.SceneID = sd.SourceSceneID
|
||||||
|
LEFT JOIN dbo.Chapters sc ON sc.ChapterID = ss.ChapterID
|
||||||
|
LEFT JOIN dbo.Books sb ON sb.BookID = sc.BookID
|
||||||
|
LEFT JOIN dbo.Scenes ts ON ts.SceneID = sd.TargetSceneID
|
||||||
|
LEFT JOIN dbo.Chapters tc ON tc.ChapterID = ts.ChapterID
|
||||||
|
LEFT JOIN dbo.Books tb ON tb.BookID = tc.BookID
|
||||||
|
WHERE sd.ProjectID = @ProjectID
|
||||||
|
OR sb.ProjectID = @ProjectID
|
||||||
|
OR tb.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE re
|
||||||
|
FROM dbo.RelationshipEvents re
|
||||||
|
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
|
||||||
|
WHERE cr.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.CharacterRelationships WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE ck
|
||||||
|
FROM dbo.CharacterKnowledge ck
|
||||||
|
LEFT JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID
|
||||||
|
LEFT JOIN dbo.Scenes s ON s.SceneID = ck.SceneID
|
||||||
|
LEFT JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
LEFT JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
LEFT JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ck.StoryAssetID
|
||||||
|
LEFT JOIN dbo.PlotThreads pt ON pt.PlotThreadID = ck.PlotThreadID
|
||||||
|
LEFT JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
|
||||||
|
WHERE ch.ProjectID = @ProjectID
|
||||||
|
OR b.ProjectID = @ProjectID
|
||||||
|
OR sa.ProjectID = @ProjectID
|
||||||
|
OR pl.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE cae
|
||||||
|
FROM dbo.CharacterAttributeEvents cae
|
||||||
|
INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID
|
||||||
|
WHERE ch.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sc
|
||||||
|
FROM dbo.SceneCharacters sc
|
||||||
|
LEFT JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID
|
||||||
|
LEFT JOIN dbo.Scenes s ON s.SceneID = sc.SceneID
|
||||||
|
LEFT JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
LEFT JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE ch.ProjectID = @ProjectID
|
||||||
|
OR b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE acec
|
||||||
|
FROM dbo.AssetCustodyEventCharacters acec
|
||||||
|
INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID
|
||||||
|
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
|
||||||
|
WHERE sa.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE ace
|
||||||
|
FROM dbo.AssetCustodyEvents ace
|
||||||
|
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
|
||||||
|
WHERE sa.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE ad
|
||||||
|
FROM dbo.AssetDependencies ad
|
||||||
|
INNER JOIN dbo.StoryAssets sourceAsset ON sourceAsset.StoryAssetID = ad.SourceAssetID
|
||||||
|
INNER JOIN dbo.StoryAssets targetAsset ON targetAsset.StoryAssetID = ad.TargetAssetID
|
||||||
|
WHERE sourceAsset.ProjectID = @ProjectID
|
||||||
|
OR targetAsset.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE ae
|
||||||
|
FROM dbo.AssetEvents ae
|
||||||
|
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
|
||||||
|
WHERE sa.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sal
|
||||||
|
FROM dbo.SceneAssetLocations sal
|
||||||
|
LEFT JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID
|
||||||
|
LEFT JOIN dbo.Scenes s ON s.SceneID = sal.SceneID
|
||||||
|
LEFT JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
LEFT JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE sa.ProjectID = @ProjectID
|
||||||
|
OR b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE petpl
|
||||||
|
FROM dbo.PlotEventTargetPlotLines petpl
|
||||||
|
LEFT JOIN dbo.ThreadEvents te ON te.ThreadEventID = petpl.PlotEventID
|
||||||
|
LEFT JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
|
||||||
|
LEFT JOIN dbo.PlotLines eventPlotLine ON eventPlotLine.PlotLineID = pt.PlotLineID
|
||||||
|
LEFT JOIN dbo.PlotLines targetPlotLine ON targetPlotLine.PlotLineID = petpl.PlotLineID
|
||||||
|
WHERE eventPlotLine.ProjectID = @ProjectID
|
||||||
|
OR targetPlotLine.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE te
|
||||||
|
FROM dbo.ThreadEvents te
|
||||||
|
LEFT JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
|
||||||
|
LEFT JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
|
||||||
|
LEFT JOIN dbo.PlotLines target ON target.PlotLineID = te.TargetPlotLineID
|
||||||
|
WHERE pl.ProjectID = @ProjectID
|
||||||
|
OR target.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE pt
|
||||||
|
FROM dbo.PlotThreads pt
|
||||||
|
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
|
||||||
|
WHERE pl.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
UPDATE dbo.PlotLines
|
||||||
|
SET ParentPlotLineID = NULL,
|
||||||
|
EmergesFromPlotLineID = NULL
|
||||||
|
WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.PlotLines WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.TimelineViewPresets WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE smv
|
||||||
|
FROM dbo.SceneMetricValues smv
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE sp
|
||||||
|
FROM dbo.ScenePurposes sp
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE so
|
||||||
|
FROM dbo.SceneOutcomes so
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = so.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
UPDATE dbo.Characters
|
||||||
|
SET AgeReferenceSceneID = NULL
|
||||||
|
WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
UPDATE s
|
||||||
|
SET PrimaryLocationID = NULL
|
||||||
|
FROM dbo.Scenes s
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
UPDATE dbo.StoryAssets
|
||||||
|
SET CurrentLocationID = NULL
|
||||||
|
WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryAssets WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
UPDATE dbo.Locations
|
||||||
|
SET ParentLocationID = NULL
|
||||||
|
WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE lr
|
||||||
|
FROM dbo.LocationRelationships lr
|
||||||
|
INNER JOIN dbo.Locations fromLocation ON fromLocation.LocationID = lr.FromLocationID
|
||||||
|
INNER JOIN dbo.Locations toLocation ON toLocation.LocationID = lr.ToLocationID
|
||||||
|
WHERE fromLocation.ProjectID = @ProjectID
|
||||||
|
OR toLocation.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.Locations WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE s
|
||||||
|
FROM dbo.Scenes s
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE c
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
WHERE b.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.Books WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.Characters WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE gpmm
|
||||||
|
FROM dbo.GenreMetricPresetMetrics gpmm
|
||||||
|
INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = gpmm.MetricTypeID
|
||||||
|
WHERE mt.ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.SceneMetricTypes WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.CharacterAttributeTypes WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.AssetStates WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
DELETE FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID;
|
||||||
|
DELETE FROM dbo.Projects WHERE ProjectID = @ProjectID;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
IF @@TRANCOUNT > 0
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
|
||||||
|
THROW;
|
||||||
|
END CATCH
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -80,7 +80,13 @@ else
|
|||||||
var archivedDateLabel = project.ArchivedDate?.ToLocalTime().ToString("yyyy-MM-dd");
|
var archivedDateLabel = project.ArchivedDate?.ToLocalTime().ToString("yyyy-MM-dd");
|
||||||
<article class="plain-card">
|
<article class="plain-card">
|
||||||
<h3>@project.ProjectName</h3>
|
<h3>@project.ProjectName</h3>
|
||||||
<p class="eyebrow">Archived@if (!string.IsNullOrWhiteSpace(archivedDateLabel)) { <span> / @archivedDateLabel</span> }</p>
|
<p class="eyebrow">
|
||||||
|
Archived
|
||||||
|
@if (!string.IsNullOrWhiteSpace(archivedDateLabel))
|
||||||
|
{
|
||||||
|
<span> / @archivedDateLabel</span>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
<p>@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
|
<p>@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@project.ProjectID">Download backup</a>
|
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@project.ProjectID">Download backup</a>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user