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

Create controller and service for options/code lists #13046

Merged
merged 19 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ public ActionResult GetOptionListIds(string org, string app)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
string[] optionListIds = altinnAppGitRepository.GetOptionListIds();
string[] optionListIds = altinnAppGitRepository.GetOptionsListIds();
return Ok(optionListIds);
}
catch (LibGit2Sharp.NotFoundException)
Expand Down
127 changes: 127 additions & 0 deletions backend/src/Designer/Controllers/OptionsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using LibGit2Sharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers;

/// <summary>
/// Controller containing actions related to options (code lists).
/// </summary>
[ApiController]
[Authorize]
[AutoValidateAntiforgeryToken]
[Route("designer/api/{org}/{repo:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/options")]
public class OptionsController : ControllerBase
{
private readonly IOptionsService _optionsService;

/// <summary>
/// Initializes a new instance of the <see cref="OptionsController"/> class.
/// </summary>
/// <param name="optionsService">The options service.</param>
public OptionsController(IOptionsService optionsService)
{
_optionsService = optionsService;
}

/// <summary>
/// Fetches the IDs of the options lists belonging to the app.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="repo">Application identifier which is unique within an organisation.</param>
/// <returns>Array of options list's IDs. Empty array if none are found</returns>
[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<string[]> GetOptionsListIds(string org, string repo)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

string[] optionsListIds = _optionsService.GetOptionsListIds(org, repo, developer);

return Ok(optionsListIds);
}

/// <summary>
/// Fetches a specific option list.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="repo">Application identifier which is unique within an organisation.</param>
/// <param name="optionsListId">Name of the option list.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Route("{optionsListId}")]
public async Task<ActionResult<List<Option>>> GetOptionsList(string org, string repo, [FromRoute] string optionsListId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

try
{
List<Option> optionsList = await _optionsService.GetOptionsList(org, repo, developer, optionsListId, cancellationToken);
return Ok(optionsList);
}
catch (NotFoundException ex)
{
return NotFound(ex.Message);
}
}

/// <summary>
/// Creates or overwrites an options list.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="repo">Application identifier which is unique within an organisation.</param>
/// <param name="optionsListId">Name of the options list.</param>
/// <param name="payload">Contents of the options list.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpPut]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("{optionsListId}")]
public async Task<ActionResult> CreateOrOverwriteOptionsList(string org, string repo, [FromRoute] string optionsListId, [FromBody] List<Option> payload, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

var newOptionsList = await _optionsService.CreateOrOverwriteOptionsList(org, repo, developer, optionsListId, payload, cancellationToken);

return Ok(newOptionsList);
}

/// <summary>
/// Deletes an option list.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="repo">Application identifier which is unique within an organisation.</param>
/// <param name="optionsListId">Name of the option list.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpDelete]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Route("{optionsListId}")]
public async Task<ActionResult> DeleteOptionsList(string org, string repo, [FromRoute] string optionsListId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

bool optionsListExists = await _optionsService.OptionsListExists(org, repo, developer, optionsListId, cancellationToken);
if (optionsListExists)
{
_optionsService.DeleteOptionsList(org, repo, developer, optionsListId);
}

return Ok($"The options file {optionsListId}.json has been deleted.");
}
}
4 changes: 2 additions & 2 deletions backend/src/Designer/Controllers/PreviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ public async Task<ActionResult<string>> GetOptions(string org, string app, strin
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
string options = await altinnAppGitRepository.GetOptions(optionListId, cancellationToken);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken);
return Ok(options);
}
catch (NotFoundException)
Expand Down Expand Up @@ -883,7 +883,7 @@ public async Task<ActionResult<string>> GetOptionsForInstance(string org, string
// TODO: Need code to get dynamic options list based on language and source?
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
string options = await altinnAppGitRepository.GetOptions(optionListId, cancellationToken);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken);
return Ok(options);
}
catch (NotFoundException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,42 +703,79 @@ public async Task<string> GetAppFrontendCshtml(CancellationToken cancellationTok
}

/// <summary>
/// Gets the options list with the provided id.
/// <param name="optionsListId">The id of the options list to fetch.</param>
/// Gets a list of file names from the Options folder representing the available options lists.
/// </summary>
/// <returns>A list of option list names.</returns>
public string[] GetOptionsListIds()
{
string optionsFolder = Path.Combine(OptionsFolderPath);
if (!DirectoryExistsByRelativePath(optionsFolder))
{
throw new NotFoundException("Options folder not found.");
}

string[] fileNames = GetFilesByRelativeDirectory(optionsFolder);
List<string> optionsListIds = [];
foreach (string fileName in fileNames.Select(Path.GetFileNameWithoutExtension))
{
optionsListIds.Add(fileName);
}

return optionsListIds.ToArray();
}

/// <summary>
/// Gets a specific options list with the provided id.
/// </summary>
/// <param name="optionsListId">The name of the options list to fetch.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The options list as a string.</returns>
/// </summary>
public async Task<string> GetOptions(string optionsListId, CancellationToken cancellationToken = default)
public async Task<string> GetOptionsList(string optionsListId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionsListId}.json");
if (!FileExistsByRelativePath(optionsFilePath))
{
throw new NotFoundException("Options file not found.");
throw new NotFoundException($"Options file {optionsListId}.json was not found.");
}
string fileContent = await ReadTextByRelativePathAsync(optionsFilePath, cancellationToken);

return fileContent;
}

/// <summary>
/// Gets a list of file names from the Options folder representing the available options lists.
/// <returns>A list of option list names.</returns>
/// Creates or overwrites the options list with the provided id.
/// If the options list already exists, it will be overwritten.
/// </summary>
public string[] GetOptionListIds()
/// <param name="optionsListId">The name of the options list to create.</param>
/// <param name="payload">The contents of the new options list as a string</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The new options list as a string.</returns>
public async Task<string> CreateOrOverwriteOptionsList(string optionsListId, string payload, CancellationToken cancellationToken = default)
{
string optionsFolder = Path.Combine(OptionsFolderPath);
if (!DirectoryExistsByRelativePath(optionsFolder))
{
throw new NotFoundException("Options folder not found.");
}
string[] fileNames = GetFilesByRelativeDirectory(optionsFolder);
List<string> optionListIds = new();
foreach (string fileName in fileNames.Select(Path.GetFileNameWithoutExtension))
cancellationToken.ThrowIfCancellationRequested();

string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionsListId}.json");
await WriteTextByRelativePathAsync(optionsFilePath, payload, true, cancellationToken);
string fileContent = await ReadTextByRelativePathAsync(optionsFilePath, cancellationToken);

return fileContent;
}

/// <summary>
/// Deletes the option list with the provided id.
/// </summary>
/// <param name="optionsListId">The name of the option list to create.</param>
public void DeleteOptionsList(string optionsListId)
{
string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionsListId}.json");
if (!FileExistsByRelativePath(optionsFilePath))
{
optionListIds.Add(fileName);
throw new NotFoundException($"Options file {optionsListId}.json was not found.");
}

return optionListIds.ToArray();
DeleteFileByRelativePath(optionsFilePath);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions backend/src/Designer/Infrastructure/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddTransient<ISigningCredentialsResolver, SigningCredentialsResolver>();
services.AddTransient<ILanguagesService, LanguagesService>();
services.AddTransient<ITextsService, TextsService>();
services.AddTransient<IOptionsService, OptionsService>();
services.AddTransient<IEnvironmentsService, EnvironmentsService>();
services.AddHttpClient<IOrgService, OrgService>();
services.AddTransient<IAppDevelopmentService, AppDevelopmentService>();
Expand Down
37 changes: 37 additions & 0 deletions backend/src/Designer/Models/Option.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis;

namespace Altinn.Studio.Designer.Models;

/// <summary>
/// Options used in checkboxes, radio buttons and dropdowns.
/// </summary>
public class Option
{
/// <summary>
/// Value that connects the option to the data model.
/// </summary>
[Required]
[JsonPropertyName("value")]
public string Value { get; set; }

/// <summary>
/// Label to present to the user.
/// </summary>
[Required]
[JsonPropertyName("label")]
public string Label { get; set; }

/// <summary>
/// Description, typically displayed below the label.
/// </summary>
[JsonPropertyName("description")]
public Optional<string> Description { get; set; }

/// <summary>
/// Help text, typically wrapped inside a popover.
/// </summary>
[JsonPropertyName("helpText")]
public Optional<string> HelpText { get; set; }
}
93 changes: 93 additions & 0 deletions backend/src/Designer/Services/Implementation/OptionsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using LibGit2Sharp;

namespace Altinn.Studio.Designer.Services.Implementation;

/// <summary>
/// Service for handling options lists (code lists).
/// </summary>
public class OptionsService : IOptionsService
{
private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory;

/// <summary>
/// Constructor
/// </summary>
/// <param name="altinnGitRepositoryFactory">IAltinnGitRepository</param>
public OptionsService(IAltinnGitRepositoryFactory altinnGitRepositoryFactory)
{
_altinnGitRepositoryFactory = altinnGitRepositoryFactory;
}

/// <inheritdoc />
public string[] GetOptionsListIds(string org, string repo, string developer)
{
var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer);

try
{
string[] optionsLists = altinnAppGitRepository.GetOptionsListIds();
return optionsLists;
}
catch (NotFoundException) // Is raised if the Options folder does not exist
{
return [];
}
}

/// <inheritdoc />
public async Task<List<Option>> GetOptionsList(string org, string repo, string developer, string optionsListId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer);

string optionsListString = await altinnAppGitRepository.GetOptionsList(optionsListId, cancellationToken);
var optionsList = JsonSerializer.Deserialize<List<Option>>(optionsListString);

return optionsList;
}

/// <inheritdoc />
public async Task<List<Option>> CreateOrOverwriteOptionsList(string org, string repo, string developer, string optionsListId, List<Option> payload, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer);

var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
string payloadString = JsonSerializer.Serialize(payload, jsonOptions);

string updatedOptionsString = await altinnAppGitRepository.CreateOrOverwriteOptionsList(optionsListId, payloadString, cancellationToken);
var updatedOptions = JsonSerializer.Deserialize<List<Option>>(updatedOptionsString);

return updatedOptions;
}

/// <inheritdoc />
public void DeleteOptionsList(string org, string repo, string developer, string optionsListId)
{
var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer);

altinnAppGitRepository.DeleteOptionsList(optionsListId);
}

/// <inheritdoc />
public async Task<bool> OptionsListExists(string org, string repo, string developer, string optionsListId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

try
{
await GetOptionsList(org, repo, developer, optionsListId, cancellationToken);
return true;
}
catch (NotFoundException)
{
return false;
}
}
}
Loading
Loading