Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implementation of TextController for org level #14583

Closed
112 changes: 112 additions & 0 deletions backend/src/Designer/Controllers/Organisation/TextController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces.Organisation;
using LibGit2Sharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers.Organisation;

/// <summary>
/// Controller for text resources on organisation level
/// </summary>
[ApiController]
[Authorize]
[Route("designer/api/{org}/text")]
public class TextController : ControllerBase
{
private readonly IOrgTextsService _orgTextsService;
private const string Repo = "content";

/// <summary>
/// Initializes a new instance of the <see cref="TextController"/>/> class.
/// </summary>
/// <param name="orgTextsService">The texts service.</param>
public TextController(IOrgTextsService orgTextsService)
{
_orgTextsService = orgTextsService;
}

/// <summary>
/// Returns a JSON resource file for the given language code
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="languageCode">The resource language id (for example <code>nb, en</code>)</param>
/// <returns>The JSON config</returns>
[HttpGet]
[Route("language/{languageCode}")]
public async Task<ActionResult<TextResource>> GetResource(string org, string languageCode)
{
try
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
TextResource textResource = await _orgTextsService.GetText(org, Repo, developer, languageCode);
return Ok(textResource);
}
catch (NotFoundException)
{
return NotFound($"Text resource, resource.{languageCode}.json, could not be found.");
}

}

/// <summary>
/// Save a resource file
/// </summary>
/// <param name="jsonData">The JSON Data</param>
/// <param name="languageCode">The resource language id (for example <code>nb, en</code> )</param>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <returns>The updated resource file</returns>
[HttpPost]
[Route("language/{languageCode}")]
public async Task<ActionResult<TextResource>> CreateResource([FromBody] TextResource jsonData, string languageCode, string org)
{
try
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
await _orgTextsService.SaveText(org, Repo, developer, jsonData, languageCode);

TextResource textResource = await _orgTextsService.GetText(org, Repo, developer, languageCode);
return Ok(textResource);
}
catch (ArgumentException e)
{
return BadRequest(e.Message);
}
}

/// <summary>
/// Method to update multiple texts for given keys and a given
/// language in the text resource files in the old format.
/// Non-existing keys will be added.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="keysTexts">List of Key/Value pairs that should be updated or added if not present.</param>
/// <param name="languageCode">The languageCode for the text resource file that is being edited.</param>
/// <remarks>Temporary method that should live until old text format is replaced by the new.</remarks>
/// <returns>The updated resource file</returns>
[HttpPut]
[Route("language/{languageCode}")]
public async Task<ActionResult<TextResource>> UpdateResource(string org, [FromBody] Dictionary<string, string> keysTexts, string languageCode)
{
try
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
await _orgTextsService.UpdateTextsForKeys(org, Repo, developer, keysTexts, languageCode);

TextResource textResource = await _orgTextsService.GetText(org, Repo, developer, languageCode);
return Ok(textResource);
}
catch (ArgumentException exception)
{
return BadRequest(exception.Message);
}
catch (NotFoundException)
{
return BadRequest($"The text resource, resource.{languageCode}.json, could not be updated.");
}
}
}
10 changes: 10 additions & 0 deletions backend/src/Designer/Factories/AltinnGitRepositoryFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,15 @@ public string GetRepositoryPath(string org, string repository, string developer)
string[] paths = { _repositoriesRootDirectory, developer.AsFileName(), org.AsFileName(), repository.AsFileName() };
return Path.Combine(paths);
}

/// <summary>
/// Creates an instance of <see cref="AltinnOrgGitRepository"/>
/// </summary>
/// <returns><see cref="AltinnOrgGitRepository"/></returns>
public AltinnOrgGitRepository GetAltinnOrgGitRepository(string org, string repository, string developer)
{
var repositoryDirectory = GetRepositoryPath(org, repository, developer);
return new AltinnOrgGitRepository(org, repository, developer, _repositoriesRootDirectory, repositoryDirectory);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models;
using LibGit2Sharp;
using JsonSerializer = System.Text.Json.JsonSerializer;

namespace Altinn.Studio.Designer.Infrastructure.GitRepository;

public class AltinnOrgGitRepository : AltinnGitRepository
{
private const string LanguageResourceFolderName = "texts/";


private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};

/// <summary>
/// Initializes a new instance of the <see cref="AltinnGitRepository"/> class.
/// </summary>
/// <param name="org">Organization owning the repository identified by it's short name.</param>
/// <param name="repository">Repository name to search for schema files.</param>
/// <param name="developer">Developer that is working on the repository.</param>
/// <param name="repositoriesRootDirectory">Base path (full) for where the repository resides on-disk.</param>
/// <param name="repositoryDirectory">Full path to the root directory of this repository on-disk.</param>
public AltinnOrgGitRepository(string org, string repository, string developer, string repositoriesRootDirectory, string repositoryDirectory) : base(org, repository, developer, repositoriesRootDirectory, repositoryDirectory)
{
}

/// <summary>
/// Returns a specific text resource written in the old text format
/// based on language code from the application.
/// </summary>
/// <remarks>
/// Format of the dictionary is: &lt;textResourceElementId &lt;language, textResourceElement&gt;&gt;
/// </remarks>
public async Task<TextResource> GetText(string language, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string resourcePath = GetPathToJsonTextsFile($"resource.{language}.json");
if (!FileExistsByRelativePath(resourcePath))
{
throw new NotFoundException("Text resource file not found.");
}
string fileContent = await ReadTextByRelativePathAsync(resourcePath, cancellationToken);
TextResource textResource = JsonSerializer.Deserialize<TextResource>(fileContent, JsonOptions);

return textResource;
}

/// <summary>
/// Saves the text resource based on language code from the application.
/// </summary>
/// <param name="languageCode">Language code</param>
/// <param name="jsonTexts">text resource</param>
public async Task SaveText(string languageCode, TextResource jsonTexts)
{
string fileName = $"resource.{languageCode}.json";
string textsFileRelativeFilePath = GetPathToJsonTextsFile(fileName);
string texts = JsonSerializer.Serialize(jsonTexts, JsonOptions);
await WriteTextByRelativePathAsync(textsFileRelativeFilePath, texts);
}

private static string GetPathToJsonTextsFile(string fileName)
{
return string.IsNullOrEmpty(fileName) ? LanguageResourceFolderName : Path.Combine(LanguageResourceFolderName, fileName);
}
}
3 changes: 3 additions & 0 deletions backend/src/Designer/Infrastructure/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
using Altinn.Studio.Designer.Repository.ORMImplementation;
using Altinn.Studio.Designer.Repository.ORMImplementation.Data;
using Altinn.Studio.Designer.Services.Implementation;
using Altinn.Studio.Designer.Services.Implementation.Organisation;
using Altinn.Studio.Designer.Services.Implementation.Preview;
using Altinn.Studio.Designer.Services.Implementation.ProcessModeling;
using Altinn.Studio.Designer.Services.Interfaces;
using Altinn.Studio.Designer.Services.Interfaces.Organisation;
using Altinn.Studio.Designer.Services.Interfaces.Preview;
using Altinn.Studio.Designer.TypedHttpClients.ImageClient;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -76,6 +78,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddTransient<IInstanceService, InstanceService>();
services.AddTransient<IProcessModelingService, ProcessModelingService>();
services.AddTransient<IImagesService, ImagesService>();
services.AddTransient<IOrgTextsService, OrgTextsService>();
services.RegisterDatamodeling(configuration);

return services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using Altinn.Studio.Designer.Services.Interfaces.Organisation;

namespace Altinn.Studio.Designer.Services.Implementation.Organisation;

public class OrgTextsService : IOrgTextsService
{
private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory;
private readonly IApplicationMetadataService _applicationMetadataService;

/// <summary>
/// Constructor
/// </summary>
/// <param name="altinnGitRepositoryFactory">IAltinnGitRepository</param>
/// <param name="applicationMetadataService">IApplicationMetadataService</param>
public OrgTextsService(IAltinnGitRepositoryFactory altinnGitRepositoryFactory, IApplicationMetadataService applicationMetadataService)
{
_altinnGitRepositoryFactory = altinnGitRepositoryFactory;
_applicationMetadataService = applicationMetadataService;
}

/// <inheritdoc />
public async Task<TextResource> GetText(string org, string repo, string developer, string languageCode)
{
AltinnOrgGitRepository altinnOrgGitRepository = _altinnGitRepositoryFactory.GetAltinnOrgGitRepository(org, repo, developer);

TextResource texts = await altinnOrgGitRepository.GetText(languageCode);

return texts;
}

/// <inheritdoc />
public async Task SaveText(string org, string repo, string developer, TextResource textResource, string languageCode)
{
AltinnOrgGitRepository altinnOrgGitRepository = _altinnGitRepositoryFactory.GetAltinnOrgGitRepository(org, repo, developer);

string[] duplicateKeys = textResource.Resources.GroupBy(tre => tre.Id).Where(grp => grp.Count() > 1).Select(grp => grp.Key).ToArray();
if (duplicateKeys.Length > 0)
{
throw new ArgumentException($"Text keys must be unique. Please review keys: {string.Join(", ", duplicateKeys)}");
}

// updating application metadata with appTitle.
TextResourceElement appTitleResourceElement = textResource.Resources.FirstOrDefault(tre => tre.Id == "appName" || tre.Id == "ServiceName");

if (appTitleResourceElement != null && !string.IsNullOrEmpty(appTitleResourceElement.Value))
{
await _applicationMetadataService.UpdateAppTitleInAppMetadata(org, repo, "nb", appTitleResourceElement.Value);
}
else
{
throw new ArgumentException("The application name must be a value.");
}

await altinnOrgGitRepository.SaveText(languageCode, textResource);
}

/// <inheritdoc />
public async Task UpdateTextsForKeys(string org, string repo, string developer, Dictionary<string, string> keysTexts, string languageCode)
{
AltinnOrgGitRepository altinnOrgGitRepository = _altinnGitRepositoryFactory.GetAltinnOrgGitRepository(org, repo, developer);
TextResource textResourceObject = await altinnOrgGitRepository.GetText(languageCode);

// handle if file not already exist
foreach (KeyValuePair<string, string> kvp in keysTexts)
{
TextResourceElement textResourceContainsKey = textResourceObject.Resources.Find(textResourceElement => textResourceElement.Id == kvp.Key);
if (textResourceContainsKey is null)
{
textResourceObject.Resources.Insert(0, new TextResourceElement() { Id = kvp.Key, Value = kvp.Value });
}
else
{
int indexTextResourceElementUpdateKey = textResourceObject.Resources.IndexOf(textResourceContainsKey);
if (textResourceContainsKey.Variables == null)
{
textResourceObject.Resources[indexTextResourceElementUpdateKey] = new TextResourceElement { Id = kvp.Key, Value = kvp.Value };
}
else
{
List<TextResourceVariable> variables = textResourceContainsKey.Variables;
textResourceObject.Resources[indexTextResourceElementUpdateKey] = new TextResourceElement { Id = kvp.Key, Value = kvp.Value, Variables = variables };
}
}
}

await altinnOrgGitRepository.SaveText(languageCode, textResourceObject);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,26 @@ public interface IAltinnGitRepositoryFactory
{
/// <summary>
/// Creates an instance of <see cref="AltinnGitRepository"/>
/// </summary>
/// </summary>
/// <param name="org">The organization owning the repository identfied by it's short name as defined in Gitea.</param>
/// <param name="repository">The name of the repository as specified in Gitea.</param>
/// <param name="developer">The user name of the developer working on the repository.</param>
AltinnGitRepository GetAltinnGitRepository(string org, string repository, string developer);

/// <summary>
/// Creates an instance of <see cref="AltinnAppGitRepository"/>
/// </summary>
/// </summary>
/// <param name="org">The organization owning the repository identfied by it's short name as defined in Gitea.</param>
/// <param name="repository">The name of the repository as specified in Gitea.</param>
/// <param name="developer">The user name of the developer working on the repository.</param>
AltinnAppGitRepository GetAltinnAppGitRepository(string org, string repository, string developer);

/// <summary>
/// Creates an instance of <see cref="AltinnOrgGitRepository"/>
/// </summary>
/// <param name="org">The organization owning the repository identfied by it's short name as defined in Gitea.</param>
/// <param name="repository">The name of the repository as specified in Gitea.</param>
/// <param name="developer">The user name of the developer working on the repository.</param>
AltinnOrgGitRepository GetAltinnOrgGitRepository(string org, string repository, string developer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models;

namespace Altinn.Studio.Designer.Services.Interfaces.Organisation;

public interface IOrgTextsService
{
/// <summary>
/// Gets texts file in old format in app repository according to
/// specified languageCode.
/// </summary>
/// <param name="org">Organisation</param>
/// <param name="repo">Repository</param>
/// <param name="developer">Username of developer</param>
/// <param name="languageCode">LanguageCode</param>
/// <returns>The text file</returns>
public Task<TextResource> GetText(string org, string repo, string developer, string languageCode);

/// <summary>
/// Saves text resource in old format.
/// </summary>
/// <param name="org">Organisation</param>
/// <param name="repo">Repository</param>
/// <param name="developer">Username of developer</param>
/// <param name="textResource">The text resource to be saved</param>
/// <param name="languageCode">LanguageCode</param>
/// <returns></returns>
public Task SaveText(string org, string repo, string developer, TextResource textResource, string languageCode);

/// <summary>
/// Updates values for
/// </summary>
/// <param name="org">Organisation</param>
/// <param name="repo">Repository</param>
/// <param name="developer">Username of developer</param>
/// <param name="keysTexts">KeysTexts</param>
/// <param name="languageCode">LanguageCode</param>
/// <returns></returns>
public Task UpdateTextsForKeys(string org, string repo, string developer, Dictionary<string, string> keysTexts, string languageCode);
}