From 62c82345772aeb9aab3a66deab193421f5f8c1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?= <janis.daehne@informatik.uni-halle.de> Date: Thu, 20 Mar 2025 17:12:03 +0100 Subject: [PATCH] - added function to create an exercise test with asset --- src/ClientServer/Config/Constants.cs | 2 +- .../Core/Exercises/DoExerciseController.cs | 2 + .../Exercises/ExerciseEditorController.cs | 169 +++++++++++++++++- .../Core/Exercises/ReleaseController.cs | 3 +- src/ClientServer/Helpers/Files.cs | 39 ++++ 5 files changed, 212 insertions(+), 3 deletions(-) diff --git a/src/ClientServer/Config/Constants.cs b/src/ClientServer/Config/Constants.cs index 64ed0a3..edbbb1c 100644 --- a/src/ClientServer/Config/Constants.cs +++ b/src/ClientServer/Config/Constants.cs @@ -13,7 +13,7 @@ namespace ClientServer.Helpers /// </summary> public static class Constants { - public static string VersionString = "2.24.0"; + public static string VersionString = "2.25.0"; /// <summary> /// this is only set once at program.cs!! diff --git a/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs b/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs index a7d9817..035ec00 100644 --- a/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs +++ b/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs @@ -562,6 +562,7 @@ namespace ClientServer.Controllers.Core.Exercises /// <summary> /// returns the user solution for the given release (exercise) and plang + /// e.g. used for getting solution after release has finished (consecutive editor) /// /// do not check if only visible to owner because this might be need in other controllers /// this does not modify the state... @@ -3767,6 +3768,7 @@ namespace ClientServer.Controllers.Core.Exercises /// <summary> /// the datetime when the version was created + /// can be null when receiving from frontend, only ever set on backend /// </summary> public DateTime LastUpdatedAt { get; set; } diff --git a/src/ClientServer/Controllers/Core/Exercises/ExerciseEditorController.cs b/src/ClientServer/Controllers/Core/Exercises/ExerciseEditorController.cs index 31514b2..62331b1 100644 --- a/src/ClientServer/Controllers/Core/Exercises/ExerciseEditorController.cs +++ b/src/ClientServer/Controllers/Core/Exercises/ExerciseEditorController.cs @@ -989,6 +989,7 @@ namespace ClientServer.Controllers.Core.Exercises } + //TODO remove var tests = new List<Test>(); foreach (ExerciseTestForBackend frontendTest in exerciseFromFrontend.Tests) @@ -1305,7 +1306,7 @@ namespace ClientServer.Controllers.Core.Exercises { await _context.SaveChangesAsync(); - Thread.Sleep(2000); + //Thread.Sleep(2000); //now main files... if (mainClassFiles.Count > 0) @@ -1763,7 +1764,151 @@ namespace ClientServer.Controllers.Core.Exercises Response.WriteAsync( Jc.Serialize(new BasicResponseWithData<int>(ResponseCode.Ok, "", exCopy.Id))); } + + //TODO we don't use file upload here but file bytes content as json --> request limits + [HttpPost("create/test/{exerciseId}")] + public async Task CreateExerciseTestWithData(int exerciseId, [FromBody] ExerciseTestFromFrontendWithData testFromFrontendWithData) + { + if (!await base.IsLoggedIn()) return; + + int userId = GetUserId(); + + List<TestType> allTestTypes = await _context.TestTypes.ToListAsync(); + + var testType = allTestTypes.FirstOrDefault(p => p.Id == testFromFrontendWithData.TestTypeId); + + if (testType == null) + { + await + Response.WriteAsync( + Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "test type not found"))); + return; + } + + Exercise oldExercise = await _context.Exercises + .Where(p => p.Id == exerciseId) + .FirstOrDefaultAsync(); + + if (await _HasPermissionAndIsValid(oldExercise, userId, false) == false) return; + + var newTest = new Test() + { + Content = testFromFrontendWithData.Content, + DisplayName = testFromFrontendWithData.DisplayName ?? "", + IsSubmitTest = testFromFrontendWithData.IsSubmitTest, + MaxPoints = testFromFrontendWithData.MaxPoints, + DisplayIndex = testFromFrontendWithData.DisplayIndex, + TestSettings = new TestSettings() + { + MemoryLimitInKb = testFromFrontendWithData.TestSettings.MemoryLimitInKb, + MaxDiskSpaceInKb = testFromFrontendWithData.TestSettings.MaxDiskSpaceInKb, + TimeoutInMs = testFromFrontendWithData.TestSettings.TimeoutInMs, + CompileTimeoutInMs = testFromFrontendWithData.TestSettings.CompileTimeoutInMs, + CompilerOptions = testFromFrontendWithData.TestSettings.CompilerOptions, + }, + TestType = testType, + Exercise = oldExercise, + }; + oldExercise.Tests.Add(newTest); + _context.Tests.Add(newTest); + + var filePreviews = new List<FilePreviewFromBackend>(); + var targetHardDrivePath = Files.GetUploadFilePath(UploadDirType.TestAssets); + FileReferenceTestAsset fileRef = null; + try + { + using (var transaction = await _context.Database.BeginTransactionAsync()) + { + try + { + + if (testFromFrontendWithData.Files.Count == 0) + { + //no files, so no save is called + await _context.SaveChangesAsync(); + transaction.Commit(); + } + else + { + foreach (var testAssetFile in testFromFrontendWithData.Files) + { + fileRef = new FileReferenceTestAsset() + { + Hash = "", + MimeType = testAssetFile.MimeType, + OriginalName = testAssetFile.DisplayName ?? "", + }; + + var connection = new TestWithFileAsAssetReference() + { + FileReferenceTestAsset = fileRef, + Test = newTest + }; + + _context.TestWithFileAsAssetReferences.Add(connection); + await _context.SaveChangesAsync(); + Tuple<string, long> _tuple = + await Files.CreateUploadedFileFromTestWithAssetFile(targetHardDrivePath, fileRef.Id, testAssetFile.Content); + fileRef.Hash = _tuple.Item1; + fileRef.SizeInBytes = _tuple.Item2; + + await _context.SaveChangesAsync(); + + filePreviews.Add(new FilePreviewFromBackend() + { + Id = fileRef.Id, + MimeType = fileRef.MimeType, + OriginalName = fileRef.OriginalName, + SizeInBytes = _tuple.Item2, + }); + + } + transaction.Commit(); + } + } + catch (Exception e) + { + transaction.Rollback(); + await base.HandleDbError(e); + return; + } + } + } + catch (Exception e) + { + await base.HandleDbError(e); + return; + } + + var testFromBackend = new ExerciseTestFromBackend() + { + Id = newTest.Id, + Content = newTest.Content, + DisplayName = newTest.DisplayName, + IsSubmitTest = newTest.IsSubmitTest, + MaxPoints = newTest.MaxPoints, + DisplayIndex = newTest.DisplayIndex, + TestTypeId = newTest.TestTypeId, + TestSettings = new TestSettingsFullBase() + { + Id = newTest.TestSettings.Id, + MemoryLimitInKb = newTest.TestSettings.MemoryLimitInKb, + MaxDiskSpaceInKb = newTest.TestSettings.MaxDiskSpaceInKb, + TimeoutInMs = newTest.TestSettings.TimeoutInMs, + CompileTimeoutInMs = newTest.TestSettings.CompileTimeoutInMs, + CompilerOptions = newTest.TestSettings.CompilerOptions, + }, + Files = filePreviews, + }; + + await + Response.WriteAsync( + Jc.Serialize( + new BasicResponseWithData<ExerciseTestFromBackend>(ResponseCode.Ok, "", + testFromBackend))); + } + /// <summary> /// checks if the exercise is valid and if the user has access to modify it /// </summary> @@ -3000,6 +3145,19 @@ namespace ClientServer.Controllers.Core.Exercises Files = new List<TestAssetFromBackendWithData>(); } } + + public class ExerciseTestFromFrontendWithData : ExerciseTestBase + { + /// <summary> + /// the assets (files) (without data because could not viewed or changed at frontend) + /// </summary> + public List<TestAssetFromFrontendWithData> Files { get; set; } + + public ExerciseTestFromFrontendWithData() + { + Files = new List<TestAssetFromFrontendWithData>(); + } + } #endregion @@ -3045,6 +3203,15 @@ namespace ClientServer.Controllers.Core.Exercises /// </summary> public string Hash { get; set; } } + + public class TestAssetFromFrontendWithData : TestAssetBase + { + /// <summary> + /// the content of the asset + /// CAN BE NULL if file could not be read + /// </summary> + public List<byte> Content { get; set; } + } #endregion diff --git a/src/ClientServer/Controllers/Core/Exercises/ReleaseController.cs b/src/ClientServer/Controllers/Core/Exercises/ReleaseController.cs index 6b65244..b2f696a 100644 --- a/src/ClientServer/Controllers/Core/Exercises/ReleaseController.cs +++ b/src/ClientServer/Controllers/Core/Exercises/ReleaseController.cs @@ -1275,7 +1275,8 @@ namespace ClientServer.Controllers.Core.Exercises HasLimitedWorkingTime = foundRelease.HasLimitedWorkingTime, ReleasedForPLangId = foundRelease.PLangId, ReleaseDurationType = (int) foundRelease.ReleaseDurationType, - ReleaseStartType = (int) foundRelease.ReleaseStartType + ReleaseStartType = (int) foundRelease.ReleaseStartType, + IsReleased = foundRelease.IsReleased, }; diff --git a/src/ClientServer/Helpers/Files.cs b/src/ClientServer/Helpers/Files.cs index 5047dfb..37b21a8 100644 --- a/src/ClientServer/Helpers/Files.cs +++ b/src/ClientServer/Helpers/Files.cs @@ -191,6 +191,45 @@ namespace ClientServer.Helpers return new Tuple<string, long>(hash,sizeInBytes); } + public static async Task<Tuple<string, long>> CreateUploadedFileFromTestWithAssetFile(string basePath, int fileId, List<byte> content) + { + string hash = ""; + long sizeInBytes = 0; + + try + { + var info = new FileInfo(Path.Combine(basePath, fileId.ToString())); + + if (info.Exists) + { + throw new Exception("file already exists"); + } + + using (var stream = new FileStream(info.FullName, FileMode.Create)) + { + stream.Write(content.ToArray(), 0, content.Count); + // await file.CopyToAsync(stream); + //CopyToAsync will set the stream position to the last position + //when we try to read the stream (e.g. md5) then we always get the empty string + stream.Seek(0, SeekOrigin.Begin); + + using (var md5 = MD5.Create()) + { + var result = md5.ComputeHash(stream); + hash = String.Join(String.Empty, result.Select(p => p.ToString("x2"))); + } + sizeInBytes = stream.Length; + } + } + catch (Exception e) + { + Console.WriteLine("[ERROR] error creating file: " + e); + throw; + } + + return new Tuple<string, long>(hash,sizeInBytes); + } + /// <summary> /// deletes the given file by path and id /// </summary> -- GitLab