Skip to content

Commit ce50f72

Browse files
annesnour03goomens
andauthored
feature: add screen endpoint & logic (#15)
* feature: add screen endpoint & logic * feat: update project title column labels for clarity * feat: refactor ScreenDataDto, ScreenColumnDto, and ScreenRowDto to use records * feat: update CORS policy * feat: introduce BsonConversionTools for BSON value conversion and refactor related methods * feat: add NavigateNestedBsonValue method for improved BSON property navigation * feat: extend ScreenDataDto to include DataType and implement GetDataType --------- Co-authored-by: Gerrit Oomens <g.oomens@uva.nl>
1 parent bb847b0 commit ce50f72

File tree

9 files changed

+408
-16
lines changed

9 files changed

+408
-16
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
name: Projects
22
entityType: Project
33
columns:
4+
- title:
5+
en: Summary
6+
nl: Samenvatting
7+
value: "{{Title}} - EC: {{EC}} - Examiner: {{Examiner.DisplayName}}"
8+
defaultSort: "Descending"
49
- property: Title
510
link: true
611
default: No title
712
- property: StartEvent
8-
- property: Student.DisplayName
913
- currentStep: true
1014
filterType: Pick
1115
default: Draft

UvA.Workflow.Api/Infrastructure/ServiceCollectionExtentions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using UvA.Workflow.Api.Submissions;
1+
using UvA.Workflow.Api.Screens;
22
using UvA.Workflow.Api.Submissions.Dtos;
33
using UvA.Workflow.Infrastructure.Database;
44
using UvA.Workflow.Persistence;
@@ -47,6 +47,8 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, I
4747

4848
services.AddScoped<IMailService, DummyMailService>();
4949

50+
services.AddScoped<ScreenDataService>();
51+
5052
services.AddSingleton(
5153
new ModelParser(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../../Examples/Projects")));
5254

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace UvA.Workflow.Api.Screens.Dtos;
2+
3+
public record ScreenDataDto(
4+
string Name,
5+
string EntityType,
6+
ScreenColumnDto[] Columns,
7+
ScreenRowDto[] Rows)
8+
{
9+
public static ScreenDataDto Create(Screen screen, ScreenColumnDto[] columns, ScreenRowDto[] rows)
10+
{
11+
return new ScreenDataDto(
12+
screen.Name,
13+
screen.EntityType ?? "",
14+
columns,
15+
rows);
16+
}
17+
}
18+
19+
public record ScreenColumnDto(
20+
int Id,
21+
BilingualString Title,
22+
string? Property,
23+
FilterType FilterType,
24+
DisplayType DisplayType,
25+
UvA.Workflow.Entities.Domain.SortDirection? DefaultSort,
26+
bool Link,
27+
DataType DataType)
28+
{
29+
public static ScreenColumnDto Create(Column column, int id)
30+
{
31+
var dataType = GetDataType(column);
32+
return new ScreenColumnDto(
33+
id,
34+
column.DisplayTitle,
35+
column.Property,
36+
column.FilterType,
37+
column.DisplayType,
38+
column.DefaultSort,
39+
column.Link,
40+
dataType);
41+
}
42+
43+
private static DataType GetDataType(Column column)
44+
{
45+
// Value templates are always text
46+
if (column.ValueTemplate != null)
47+
return DataType.String;
48+
49+
// CurrentStep is always string
50+
if (column.CurrentStep)
51+
return DataType.String;
52+
53+
// Event columns are DateTime
54+
if (column.Property != null && column.Property.EndsWith("Event"))
55+
return DataType.DateTime;
56+
57+
// Use the underlying question's data type if available
58+
if (column.Question != null)
59+
return column.Question.DataType;
60+
61+
// Default to string for anything else
62+
return DataType.String;
63+
}
64+
}
65+
66+
public record ScreenRowDto(
67+
string Id,
68+
Dictionary<int, object?> Values)
69+
{
70+
public static ScreenRowDto Create(string id, Dictionary<int, object?> values)
71+
{
72+
return new ScreenRowDto(id, values);
73+
}
74+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
using UvA.Workflow.Api.Screens.Dtos;
2+
using UvA.Workflow.Tools;
3+
4+
namespace UvA.Workflow.Api.Screens;
5+
6+
public class ScreenDataService(
7+
ModelService modelService,
8+
IWorkflowInstanceRepository repository)
9+
{
10+
public async Task<ScreenDataDto> GetScreenData(string screenName, string entityType, CancellationToken ct)
11+
{
12+
// Get the screen definition
13+
var screen = GetScreen(screenName, entityType);
14+
if (screen == null)
15+
throw new ArgumentException($"Screen '{screenName}' not found for entity type '{entityType}'");
16+
17+
// Build projection based on screen columns
18+
var projection = BuildProjection(screen, entityType);
19+
var rawData = await repository.GetAllByType(entityType, projection, ct);
20+
21+
// Process the data and apply templates/expressions
22+
var columns = screen.Columns.Select(ScreenColumnDto.Create).ToArray();
23+
var rows = ProcessRows(rawData, screen, entityType, columns);
24+
25+
return ScreenDataDto.Create(screen, columns, rows);
26+
}
27+
28+
private Screen? GetScreen(string screenName, string entityType)
29+
{
30+
if (!modelService.EntityTypes.TryGetValue(entityType, out var entity))
31+
return null;
32+
33+
return entity.Screens.GetValueOrDefault(screenName);
34+
}
35+
36+
private Dictionary<string, string> BuildProjection(Screen screen, string entityType)
37+
{
38+
if (!modelService.EntityTypes.TryGetValue(entityType, out var entity))
39+
throw new ArgumentException($"Entity type '{entityType}' not found");
40+
41+
var projection = new Dictionary<string, string>();
42+
43+
foreach (var column in screen.Columns)
44+
{
45+
if (column.CurrentStep)
46+
{
47+
projection["CurrentStep"] = "$CurrentStep";
48+
}
49+
else if (!string.IsNullOrEmpty(column.Property))
50+
{
51+
// Use EntityType.GetKey to get the correct MongoDB path
52+
var mongoPath = entity.GetKey(column.Property.Split('.')[0]);
53+
var propertyName = column.Property.Split('.')[0];
54+
55+
projection.TryAdd(propertyName, mongoPath);
56+
}
57+
58+
// If column has templates, we need to include their properties
59+
if (column.ValueTemplate != null)
60+
{
61+
foreach (var prop in column.ValueTemplate.Properties)
62+
{
63+
AddLookupToProjection(projection, prop, entity);
64+
}
65+
}
66+
}
67+
68+
return projection;
69+
}
70+
71+
private void AddLookupToProjection(Dictionary<string, string> projection, Lookup lookup, EntityType entity)
72+
{
73+
switch (lookup)
74+
{
75+
case PropertyLookup propertyLookup:
76+
var propertyName = propertyLookup.Property.Split('.')[0];
77+
var mongoPath = entity.GetKey(propertyName);
78+
projection.TryAdd(propertyName, mongoPath);
79+
break;
80+
case ComplexLookup complexLookup:
81+
// For complex lookups, we need to add properties from their arguments
82+
foreach (var arg in complexLookup.Arguments)
83+
{
84+
foreach (var prop in arg.Properties)
85+
{
86+
AddLookupToProjection(projection, prop, entity);
87+
}
88+
}
89+
90+
break;
91+
}
92+
}
93+
94+
private ScreenRowDto[] ProcessRows(
95+
List<Dictionary<string, BsonValue>> rawData,
96+
Screen screen,
97+
string entityType,
98+
ScreenColumnDto[] columns
99+
)
100+
{
101+
var rows = new List<ScreenRowDto>();
102+
103+
foreach (var rawRow in rawData)
104+
{
105+
var id = rawRow.GetValueOrDefault("_id")?.ToString() ?? "Unknown";
106+
var processedValues = new Dictionary<int, object?>();
107+
108+
// Process each column and use its ID as the key
109+
for (int i = 0; i < screen.Columns.Length; i++)
110+
{
111+
var column = screen.Columns[i];
112+
var columnId = columns[i].Id;
113+
var value = ProcessColumnValue(rawRow, column, entityType, id);
114+
processedValues[columnId] = value;
115+
}
116+
117+
rows.Add(ScreenRowDto.Create(id, processedValues));
118+
}
119+
120+
return rows.ToArray();
121+
}
122+
123+
private object? ProcessColumnValue(
124+
Dictionary<string, BsonValue> rawRow,
125+
Column column,
126+
string entityType,
127+
string instanceId
128+
)
129+
{
130+
if (column.CurrentStep)
131+
{
132+
// Return current step value or default
133+
return rawRow.GetStringValue("CurrentStep") ?? column.Default ?? "Draft";
134+
}
135+
136+
if (column.ValueTemplate != null)
137+
{
138+
// Process template - create a context and evaluate the template
139+
var context = CreateContextFromRawRow(rawRow, entityType, instanceId);
140+
return column.ValueTemplate.Execute(context);
141+
}
142+
143+
if (!string.IsNullOrEmpty(column.Property))
144+
{
145+
// Get property value from raw data
146+
var value = GetNestedPropertyValue(rawRow, column.Property);
147+
if (value != null && !value.IsBsonNull)
148+
{
149+
return BsonConversionTools.ConvertBasicBsonValue(value);
150+
}
151+
}
152+
153+
return column.Default;
154+
}
155+
156+
private ObjectContext CreateContextFromRawRow(Dictionary<string, BsonValue> rawRow, string entityType,
157+
string instanceId)
158+
{
159+
// Create a minimal WorkflowInstance for context creation
160+
// Convert the projected properties back to the expected Properties format
161+
var properties = new Dictionary<string, BsonValue>();
162+
163+
foreach (var kvp in rawRow)
164+
{
165+
if (kvp.Key == "_id" || kvp.Key == "CurrentStep" || kvp.Key.EndsWith("Event")) // TODO this is a bit iffy?
166+
continue;
167+
168+
// The key is the property name, value is the BsonValue from $Properties.{key}
169+
properties[kvp.Key] = kvp.Value;
170+
}
171+
172+
var instance = new WorkflowInstance
173+
{
174+
Id = instanceId,
175+
EntityType = entityType,
176+
Properties = properties,
177+
Events = new Dictionary<string, InstanceEvent>(),
178+
CurrentStep = rawRow.GetStringValue("CurrentStep")
179+
};
180+
181+
return modelService.CreateContext(instance);
182+
}
183+
184+
private BsonValue? GetNestedPropertyValue(Dictionary<string, BsonValue> data, string propertyPath)
185+
{
186+
var parts = propertyPath.Split('.');
187+
var rootProperty = parts[0];
188+
189+
if (!data.TryGetValue(rootProperty, out var rootValue))
190+
return null;
191+
192+
// If only one part, return the root value
193+
if (parts.Length == 1)
194+
return rootValue;
195+
196+
// Use shared utility to navigate the remaining path
197+
return BsonConversionTools.NavigateNestedBsonValue(rootValue, parts.Skip(1));
198+
}
199+
200+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using UvA.Workflow.Api.Infrastructure;
2+
using UvA.Workflow.Api.Screens.Dtos;
3+
4+
namespace UvA.Workflow.Api.Screens;
5+
6+
public class ScreensController(ScreenDataService screenDataService) : ApiControllerBase
7+
{
8+
/// <summary>
9+
/// Gets the specific screen for an instance, with column and rows
10+
/// </summary>
11+
/// <param name="entityType">The entity type to get instances for</param>
12+
/// <param name="screenName">The name of the screen configuration to use</param>
13+
/// <param name="ct">Cancellation token</param>
14+
/// <returns>Screen data with columns and rows containing the projected data</returns>
15+
[HttpGet("{entityType}/{screenName}")]
16+
public async Task<ActionResult<ScreenDataDto>> GetScreenData(
17+
string entityType,
18+
string screenName,
19+
CancellationToken ct)
20+
{
21+
try
22+
{
23+
var screenData = await screenDataService.GetScreenData(screenName, entityType, ct);
24+
return Ok(screenData);
25+
}
26+
catch (ArgumentException ex)
27+
{
28+
return NotFound("ScreenNotFound", ex.Message);
29+
}
30+
catch (Exception ex)
31+
{
32+
return Problem(
33+
detail: ex.Message,
34+
title: "Error retrieving screen data"
35+
);
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)