commit 440997e038f7fb2f9a317419a68be94f86ea6eca
Author: Vojtěch Bartička <aitakaitov@gmail.com>
Date:   Mon May 9 08:34:00 2022 +0200

    Tag merging for final annotation beta

diff --git a/Backend/Backend/Controllers/DocumentController.cs b/Backend/Backend/Controllers/DocumentController.cs
index 1e4a0a6..d8c62e9 100644
--- a/Backend/Backend/Controllers/DocumentController.cs
+++ b/Backend/Backend/Controllers/DocumentController.cs
@@ -2,11 +2,12 @@
 using Microsoft.AspNetCore.Mvc;
 using RestAPI.Utils;
 using System.Net;
-using Serilog;
 using ILogger = Serilog.ILogger;
 using Models.Documents;
 using RestAPI.Exceptions;
 using RestAPI.Authentication;
+using Models.Annotations;
+using Core.Services.AnnotationService;
 
 namespace RestAPI.Controllers;
 
@@ -16,12 +17,13 @@ public class DocumentController : Common.CommonControllerBase
 {
     private readonly IDocumentService documentService;
     private readonly ILogger logger;
+    private readonly IAnnotationService annotationService;
 
-
-    public DocumentController(IDocumentService documentService, ILogger logger)
+    public DocumentController(IDocumentService documentService, IAnnotationService annotationService, ILogger logger)
     {
         this.documentService = documentService;
         this.logger = logger;
+        this.annotationService = annotationService;
     }
 
     [HttpGet("/documents")]
@@ -180,5 +182,31 @@ public class DocumentController : Common.CommonControllerBase
         }
     }
 
+    [HttpPost("/document/{documentId}/final")]
+    [Authorize(Models.Enums.ERole.ADMINISTRATOR)]
+    [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(CreateFinalAnnotationResponse))]
+    [ProducesResponseType((int)HttpStatusCode.Forbidden)]
+    public ActionResult<CreateFinalAnnotationResponse> CreateFinalAnnotation([FromServices] ClientInfo clientInfo, Guid documentId)
+    {
+        if (clientInfo.LoggedUser == null)
+        {
+            logger.Warning("ClientInfo has null LoggerUser in [Authorized] controller");
+            return Problem();
+        }
+
+        try
+        {
+            var guid = annotationService.CreateFinalAnnotation(documentId, clientInfo.LoggedUser.Id);
+
+            return Ok(new CreateFinalAnnotationResponse()
+            {
+                FinalAnnotationId = guid
+            });
+        }
+        catch (InvalidOperationException e)
+        {
+            throw new BadRequestException(e.Message);
+        }
+    }
 }
 
diff --git a/Backend/Backend/appsettings.Development.json b/Backend/Backend/appsettings.Development.json
index 63324f2..4208291 100644
--- a/Backend/Backend/appsettings.Development.json
+++ b/Backend/Backend/appsettings.Development.json
@@ -6,7 +6,7 @@
     }
   },
   "AllowedHosts": "*",
-  "ConnectionString": "Host=localhost:5432;Database=dbo;Username=myuser;Password=password",  
+  "ConnectionString": "Host=localhost:5432;Database=dbo;Username=myuser;Password=password;Include Error Detail=true;",  
   "JwtConfig": {
     "Secret": "CJgvc9BtwJjnCExzKLB2ndtxERW3YMhqMMPNrBZuTJaskPqRVfZQHRUnbRHH9Ekp5zsaTSeM73vQSKvq48w2u8jTnKUn7Uhk2zBeD9wLz48ssKMppNC9HDTXSk6RWP5PtXfCayJ67mbUWF6aknnteLg4sDKzqYeNYjyw8Jk6",
     "Issuer": "0x00.server"
diff --git a/Backend/Core/MapperProfiles/TagProfileEF.cs b/Backend/Core/MapperProfiles/TagProfileEF.cs
index 4095ddd..7b26d24 100644
--- a/Backend/Core/MapperProfiles/TagProfileEF.cs
+++ b/Backend/Core/MapperProfiles/TagProfileEF.cs
@@ -18,6 +18,11 @@ namespace Core.MapperProfiles
             CreateMap<SubTag, SubTagInfo>();
             CreateMap<AnnotationTag, TagInstanceInfo>()
                 .ForMember(ti => ti.OccurenceId, opt => opt.MapFrom(at => at.Id));
+
+            // Ignore ID and Annotation
+            CreateMap<AnnotationTag, FinalAnnotationTag>()
+                .ForMember(fat => fat.Id, opt => opt.Ignore())
+                .ForMember(fat => fat.Annotation, opt => opt.Ignore());
         }
     }
 }
diff --git a/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs b/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs
index 4951b2e..f54d24c 100644
--- a/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs
+++ b/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs
@@ -258,8 +258,7 @@ namespace Core.Services.AnnotationService
                     for (; i < descCount; i++)
                     {
                         var node = descendantsToEdit.ElementAt(i);
-                        if (!node.Name.Contains("#text") || addedForSelection.Contains(node) || addedForSelection.Contains(node.ParentNode) ||
-                            node.ParentNode.Name == "style")
+                        if (!node.Name.Contains("#text") || addedForSelection.Contains(node) || addedForSelection.Contains(node.ParentNode) || node.ParentNode.Name == "style")
                         {
                             continue;
                         }
@@ -959,5 +958,183 @@ namespace Core.Services.AnnotationService
             annotation.State = done ? EState.DONE : EState.IN_PROGRESS;
             context.SaveChanges();
         }
+
+        public Guid CreateFinalAnnotation(Guid documentId, Guid userId)
+        {
+            var document = context.Documents.Single(d => d.Id == documentId);
+            var user = context.Users.Single(u => u.Id == userId);
+
+            // Remove existing 
+            if (context.FinalAnnotations.Any(fa => fa.Document == document))
+            {
+                var finalAnnotationOld = context.FinalAnnotations.Single(fa => fa.Document == document);
+                context.FinalAnnotations.Remove(finalAnnotationOld);
+            }
+
+            var annotations = context.Annotations
+                .Include(a => a.Document)
+                .Where(a => a.Document == document && a.State == EState.DONE)
+                .ToList();
+
+            var finalAnnotation = new FinalAnnotation()
+            {
+                Id = Guid.NewGuid(),
+                DateAssigned = DateTime.Now,
+                DateLastChanged = DateTime.Now,
+                Document = document,
+                User = user,
+                UserAssigned = user,
+                Annotations = annotations,
+                State = EState.NEW
+            };
+
+            List<AnnotationTag> annotationTagsAll = new();
+            foreach (var annotation in annotations)
+            {
+                annotationTagsAll.AddRange(context.AnnotationTags
+                    .Where(at => at.Annotation == annotation)
+                    .Include(at => at.Tag)
+                    .Include(at => at.SubTag)
+                    .Include(at => at.Annotation)
+                    .ThenInclude(a => a.User)
+                    .ToList());
+            }
+            annotationTagsAll = annotationTagsAll.OrderBy(at => at.Position).ToList();
+
+            Dictionary<Guid, List<AnnotationTag>> occurenceDict = new();
+            foreach (var annotationTag in annotationTagsAll)
+            {
+                if (occurenceDict.ContainsKey(annotationTag.Instance))
+                {
+                    occurenceDict[annotationTag.Instance].Add(annotationTag);
+                }
+                else
+                {
+                    occurenceDict[annotationTag.Instance] = new();
+                    occurenceDict[annotationTag.Instance].Add(annotationTag);
+                }
+            }
+
+            List<List<AnnotationTag>> occurenceLists = new();
+            foreach (var key in occurenceDict.Keys)
+            {
+                occurenceLists.Add(occurenceDict[key]);
+            }
+
+            List<List<AnnotationTag>> annotationTagsProcessed = new();
+            List<FinalAnnotationTag> finalAnnotationTags = new();
+            for (int i = 0; i < occurenceLists.Count; i++)
+            {
+                var occurrenceList1 = occurenceLists[i];
+                List<List<AnnotationTag>> sameLists = new();
+
+                for (int j = 0; j < occurenceLists.Count; j++)
+                {
+                    var occurrenceList2 = occurenceLists[j];
+
+                    if (annotationTagsProcessed.Contains(occurrenceList2))
+                    {
+                        continue;
+                    }
+
+                    if (SelectionsAreSame(occurrenceList1, occurrenceList2))
+                    {
+                        sameLists.Add(occurrenceList2);
+                    }
+                }
+
+                // This means that this occurrence ahs already been processed as matching a previous tag
+                if (sameLists.Count() == 0)
+                {
+                    continue;
+                }
+
+                List<User> relatedUsers = new();
+                foreach (var list in sameLists)
+                {
+                    relatedUsers.Add(list[0].Annotation.User);
+                }
+
+                foreach (var tag in sameLists[0])
+                {
+                    finalAnnotationTags.Add(new()
+                    {
+                        Id = Guid.NewGuid(),
+                        Tag = tag.Tag,
+                        SubTag = tag.SubTag,
+                        Annotation = finalAnnotation,
+                        SelectedText = tag.SelectedText,
+                        Sentiment = tag.Sentiment,
+                        Instance = tag.Instance,
+                        IsFinal = sameLists.Count == annotations.Count,
+                        Length = tag.Length,
+                        Position = tag.Position,
+                        Note = "",
+                        Users = relatedUsers
+                    });
+                }
+
+                annotationTagsProcessed.AddRange(sameLists);
+            }
+
+            context.FinalAnnotations.Add(finalAnnotation);
+            context.SaveChanges();
+            context.FinalAnnotationTags.AddRange(finalAnnotationTags);
+
+            context.SaveChanges();
+            return finalAnnotation.Id;
+        }
+
+        private bool SelectionsAreSame(List<AnnotationTag> list1, List<AnnotationTag> list2)
+        {
+            if (list1.Count != list2.Count)
+            {
+                return false;
+            }
+
+            bool sentimentEnabled = list1[0].Tag.SentimentEnabled;
+
+            if (sentimentEnabled)
+            {
+                for (int i = 0; i < list1.Count; i++)
+                {
+                    var tag1 = list1[i];
+                    var tag2 = list2[i];
+                    if (tag1.Position == tag2.Position &&
+                        tag1.Length == tag2.Length &&
+                        tag1.Sentiment == tag2.Sentiment &&
+                        tag1.Tag == tag2.Tag &&
+                        tag1.SubTag == tag2.SubTag)
+                    {
+                        continue;
+                    }
+                    else
+                    {
+                        return false;
+                    }
+                }
+            }
+            else
+            {
+                for (int i = 0; i < list1.Count; i++)
+                {
+                    var tag1 = list1[i];
+                    var tag2 = list2[i];
+                    if (tag1.Position == tag2.Position &&
+                        tag1.Length == tag2.Length &&
+                        tag1.Tag == tag2.Tag &&
+                        tag1.SubTag == tag2.SubTag)
+                    {
+                        continue;
+                    }
+                    else
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            return true;
+        }
     }
 }
diff --git a/Backend/Core/Services/AnnotationService/IAnnotationService.cs b/Backend/Core/Services/AnnotationService/IAnnotationService.cs
index d77ce53..f20fcee 100644
--- a/Backend/Core/Services/AnnotationService/IAnnotationService.cs
+++ b/Backend/Core/Services/AnnotationService/IAnnotationService.cs
@@ -18,5 +18,6 @@ namespace Core.Services.AnnotationService
         public void AddNoteToAnnotation(Guid annotationId, Guid userId, ERole userRole, AddNoteToAnnotationRequest request);
         public void SetTagInstanceSentiment(Guid annotationId, Guid instanceId, Guid userId, ERole userRole, ETagSentiment sentiment);
         public void MarkAnnotationAsDone(Guid annotationId, Guid userId, ERole userRole, bool done);
+        public Guid CreateFinalAnnotation(Guid documentId, Guid userId);
     }
 }
diff --git a/Backend/Models/Annotations/CreateFinalAnnotationResponse.cs b/Backend/Models/Annotations/CreateFinalAnnotationResponse.cs
new file mode 100644
index 0000000..54f455c
--- /dev/null
+++ b/Backend/Models/Annotations/CreateFinalAnnotationResponse.cs
@@ -0,0 +1,13 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Models.Annotations
+{
+    public class CreateFinalAnnotationResponse
+    {
+        public Guid FinalAnnotationId { get; set; }
+    }
+}
