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