Skip to content

Commit

Permalink
Add Competence Document KPI matrix (#202)
Browse files Browse the repository at this point in the history
* Add KpiMatrixAssignment.cs

* Add KpiMatrixOutcome.cs

* Add KpiMatrixOutcomeStatus.cs

* Add KpiMatrixCollection.cs

* Remove old repo code (incorrectly added)

* Add KpiMatrixComponent.cs

* Rewrite data format (not done)

* Add component to CompetenceDocumentService.cs

* Remove old KpiMatrixCollection.cs

* Fix naming issues in kpi matrix component

* Fix data format issues in getLegend method

* Add outcomes to components

* Implement outcomes in KpiMatrixComponent.cs

* Change get color logic

* Fix color issues in kpi matrix cells

* Fix spacing and remove comments

* Fix unit tests with new parameter

* Fix unit test issue in kpi matrix component

* Implement PR feedback

* Implement PR feedback

* Add competence profile component create table cell method

* Implement create table cell method

* Fix borders in kpi matrix

* Add "Val" parameter for shading

* Add "Val" property to shading

* Apply shading directly to TableCellProperties

* Add default color values for shading

* Fix shading and border errors

* Revert "Fix shading and border errors"

This reverts commit 27210e6.

* Change order of borders

* Add distinct filtering in criteria

* Fix error for unit tests

* Add order by functionality

* fix kpi matrix header row bug

* fix merge request bugs

---------

Co-authored-by: Neal Geilen <[email protected]>
Co-authored-by: Bas Bakens <[email protected]>
Co-authored-by: Kalle Pronk <[email protected]>
  • Loading branch information
4 people authored Jan 12, 2024
1 parent bdbc2e9 commit bb59e9d
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ namespace Epsilon.Abstractions.Components;

public abstract class AbstractCompetenceComponent: IWordCompetenceComponent
{
protected AbstractCompetenceComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains)
protected AbstractCompetenceComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)
{
Submissions = submissions;
Domains = domains;
Outcomes = outcomes;
}

protected IAsyncEnumerable<LearningDomainSubmission> Submissions { get; set; }
protected IEnumerable<LearningDomain?> Domains { get; set; }

protected IEnumerable<LearningDomainOutcome> Outcomes { get; set; }

public abstract Task<Body?> AddToWordDocument(MainDocumentPart mainDocumentPart);
}
8 changes: 4 additions & 4 deletions Epsilon.UnitTest/Services/CompetenceDocumentServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public async Task GetDocument_ReturnsCorrectDocumentWithFilteredDates()
var userId = "testUser";
var from = DateTime.Now.AddDays(-7);
var to = DateTime.Now;
var expectedDocument = CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),});
var expectedDocument = CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),}, new List<LearningDomainOutcome>{TestDataGenerator.GenerateRandomLearningDomainOutcome(),});

_canvasResultServiceMock.Setup(service => service.GetSubmissions(userId)).Returns(_submissions);

Expand All @@ -46,7 +46,7 @@ public async Task GetDocument_ReturnsCorrectDocumentWithNoDates()
{
// Arrange
var userId = "testUser";
var expectedDocument = CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),});
var expectedDocument = CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),}, new List<LearningDomainOutcome>{TestDataGenerator.GenerateRandomLearningDomainOutcome(),});

_canvasResultServiceMock.Setup(service => service.GetSubmissions(userId)).Returns(_submissions);
// Act
Expand All @@ -61,7 +61,7 @@ public async void WriteDocument_WritesToStream()
{
// Arrange

var document = new CompetenceDocument(CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),}));
var document = new CompetenceDocument(CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),}, new List<LearningDomainOutcome>{TestDataGenerator.GenerateRandomLearningDomainOutcome(),}));
using var stream = new MemoryStream();

// Act
Expand All @@ -75,7 +75,7 @@ public async void WriteDocument_WritesToStream()
private async void ValidateOpnXmlWordGeneration()
{
// Arrange
var document = new CompetenceDocument(CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),}));
var document = new CompetenceDocument(CompetenceDocumentService.FetchComponents(_submissions, new List<LearningDomain?>(){TestDataGenerator.GenerateRandomLearningDomain(), TestDataGenerator.GenerateRandomLearningDomain(),}, new List<LearningDomainOutcome>{TestDataGenerator.GenerateRandomLearningDomainOutcome(),}));
using var stream = new MemoryStream();
await _competenceDocumentService.WriteDocument(stream, document);
using var wordprocessingDocument = WordprocessingDocument.Open(stream, false);
Expand Down
4 changes: 2 additions & 2 deletions Epsilon/Components/CompetenceProfileComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,8 @@ private static TableCell CreateTableCell(string? width, params OpenXmlElement[]
return cell;
}

public CompetenceProfileComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains)
: base(submissions, domains)
public CompetenceProfileComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)
: base(submissions, domains, outcomes)
{
}
}
208 changes: 208 additions & 0 deletions Epsilon/Components/KpiMatrixComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Epsilon.Abstractions;
using Epsilon.Abstractions.Components;

namespace Epsilon.Components;

public class KpiMatrixComponent : AbstractCompetenceComponent
{

public KpiMatrixComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)
: base(submissions, domains, outcomes)
{
}

public override async Task<Body?> AddToWordDocument(MainDocumentPart mainDocumentPart)
{
var body = mainDocumentPart.Document.Body;
// Create a table, with rows for the outcomes and columns for the assignments.
var table = new Table();


// Set table properties for formatting.
table.AppendChild(new TableProperties(
new TableWidth { Width = "0", Type = TableWidthUnitValues.Auto, }));
table.AppendChild(new TableGrid());

// Calculate the header row height based on the longest assignment name.
var headerRowHeight = 0;
var anyAssignments = false;

await foreach (var sub in Submissions)
{
anyAssignments = true;
headerRowHeight = Math.Max(headerRowHeight, sub.Assignment?.Length ?? 10);

}

if (anyAssignments)
{
headerRowHeight *= 111;
}

// Create the table header row.
var headerRow = new TableRow();
headerRow.AppendChild(new TableRowProperties(new TableRowHeight { Val = (UInt32Value)(uint)headerRowHeight, }));

// Empty top-left cell.
headerRow.AppendChild(CreateTableCell("2500", GetBorders(), null, new Paragraph(new Run(new Text(" ")))));

var index = 0;
await foreach (var sub in Submissions)
{
var shading = new Shading
{
Val = ShadingPatternValues.Clear,
Fill = index % 2 == 0
? "FFFFFF"
: "d3d3d3",
};

var cell = CreateTableCell("100", GetBorders(), shading);

cell.FirstChild.Append(new TextDirection { Val = TextDirectionValues.TopToBottomLeftToRightRotated, });

Check warning on line 65 in Epsilon/Components/KpiMatrixComponent.cs

View workflow job for this annotation

GitHub Actions / Build .NET solution

Dereference of a possibly null reference.

Check warning on line 65 in Epsilon/Components/KpiMatrixComponent.cs

View workflow job for this annotation

GitHub Actions / Unit testing

Dereference of a possibly null reference.

cell.Append(new Paragraph(new Run(new Text(sub.Assignment ?? "Not found"))));
headerRow.AppendChild(cell);


index++;
}

table.AppendChild(headerRow);


var listOfCriteria = Submissions
.SelectMany(static e => e.Criteria
.Select(static result => result )
.ToAsyncEnumerable());

// Add the outcome rows.
await foreach (var outcomeCriterion in listOfCriteria.Distinct().OrderBy(static a => a.Id))
{
var row = new TableRow();
var outcome = Outcomes.FirstOrDefault(o => o.Id == outcomeCriterion.Id);

// Create a new paragraph for outcome.Name
if (outcome != null)
{
var paragraphForOutcomeName = new Paragraph(new Run(new Text(outcome.Name)))
{
ParagraphProperties = new ParagraphProperties { Justification = new Justification { Val = JustificationValues.Center, }, },
};
row.AppendChild(CreateTableCell("2500", GetBorders(), null, paragraphForOutcomeName));
}

await foreach (var sub in Submissions)
{
var criteria = sub.Criteria.FirstOrDefault(c => c.Id == outcome?.Id);
var result = sub.Results.FirstOrDefault(r => r.Outcome.Id == outcome?.Id);

var status = GetStatus(result?.Grade, criteria?.MasteryPoints);
var fillColor = GetColor(status);
if (string.IsNullOrEmpty(fillColor))
{
fillColor = "FFFFFF"; // default color code
}

var shading = new Shading { Val = ShadingPatternValues.Clear, Fill = fillColor, };
var cell = CreateTableCell("100", GetBorders(), shading);
var text = result != null ? result.Outcome.Value.ShortName : "";
var paragraph = new Paragraph();
var run = new Run(new Text(text));
paragraph.Append(run);
paragraph.ParagraphProperties = new ParagraphProperties()
{
Justification = new Justification() { Val = JustificationValues.Center, },
};

cell.Append(paragraph);
row.AppendChild(cell);
}

table.AppendChild(row);
}

body?.Append(new Paragraph(new Run(new Text(""))));
body?.AppendChild(table);
return body;
}

private static OutcomeGradeStatus GetStatus(double? grade, double? masteryPoints)
{
if (grade.HasValue && masteryPoints.HasValue)
{
return grade.Value >= masteryPoints.Value ? OutcomeGradeStatus.Mastered : OutcomeGradeStatus.NotMastered;
}
if (!grade.HasValue && masteryPoints.HasValue)
{
return OutcomeGradeStatus.NotAssessed;
}
return OutcomeGradeStatus.NotGraded;
}

private static string GetColor(OutcomeGradeStatus status)
{
return status switch
{
OutcomeGradeStatus.Mastered => "#44F656",
OutcomeGradeStatus.NotMastered => "#FA1818",
OutcomeGradeStatus.NotAssessed => "#9F2B68",
OutcomeGradeStatus.NotGraded => "FFFFFF",
_ => "FFFFFF",
};
}

public static TableCell CreateTableCell(string? width, TableCellBorders borders, Shading? shading, params OpenXmlElement[]? elements)
{
width ??= "300";

var cell = new TableCell();

var cellProperties = new TableCellProperties()
{
TableCellBorders = (TableCellBorders)borders.CloneNode(true),
TableCellWidth = new TableCellWidth { Type = TableWidthUnitValues.Dxa, Width = width, },
// TableCellVerticalAlignment = new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center,},
};

if (shading != null)
{
cellProperties.Shading = (Shading)shading.CloneNode(true);
}

if (elements != null)
{
foreach (var element in elements)
{
cell.Append(element.CloneNode(true));
}
}
cell.TableCellProperties = cellProperties;

return cell;
}

private static TableCellBorders GetBorders()
{
return new TableCellBorders(
new TopBorder
{
Val = BorderValues.Single,
},
new LeftBorder
{
Val = BorderValues.Single,
},
new BottomBorder
{
Val = BorderValues.Single,
},
new RightBorder
{
Val = BorderValues.Single,
});
}
}
4 changes: 2 additions & 2 deletions Epsilon/Components/KpiTableComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ private static TableCell CreateTableCell(string? width, params OpenXmlElement[]
return cell;
}

public KpiTableComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains)
: base(submissions, domains)
public KpiTableComponent(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)
: base(submissions, domains, outcomes)
{
}
}
11 changes: 6 additions & 5 deletions Epsilon/Services/CompetenceDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ ILearningOutcomeCanvasResultService canvasResultService
public async Task<CompetenceDocument> GetDocument(string userId, DateTime? from = null, DateTime? to = null)
{
var submissions = _canvasResultService.GetSubmissions(userId);
submissions = submissions.Where(static s => s.Criteria.Any());
submissions = submissions.Where(static e => e.Criteria.Any());
if (from != null && to != null)
{
submissions = submissions.Where(s => s.SubmittedAt >= from && s.SubmittedAt <= to);
}

var domains = await _domainService.GetDomainsFromTenant();
var components = FetchComponents(submissions, domains);
var components = FetchComponents(submissions, domains, await _domainService.GetOutcomes());

Check warning on line 35 in Epsilon/Services/CompetenceDocumentService.cs

View workflow job for this annotation

GitHub Actions / Build .NET solution

Argument of type 'IEnumerable<LearningDomainOutcome?>' cannot be used for parameter 'outcomes' of type 'IEnumerable<LearningDomainOutcome>' in 'IAsyncEnumerable<AbstractCompetenceComponent> CompetenceDocumentService.FetchComponents(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)' due to differences in the nullability of reference types.

Check warning on line 35 in Epsilon/Services/CompetenceDocumentService.cs

View workflow job for this annotation

GitHub Actions / Unit testing

Argument of type 'IEnumerable<LearningDomainOutcome?>' cannot be used for parameter 'outcomes' of type 'IEnumerable<LearningDomainOutcome>' in 'IAsyncEnumerable<AbstractCompetenceComponent> CompetenceDocumentService.FetchComponents(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)' due to differences in the nullability of reference types.

return new CompetenceDocument(components);
}
Expand All @@ -53,9 +53,10 @@ public async Task<WordprocessingDocument> WriteDocument(Stream stream, Competenc
return wordDocument;
}

public static async IAsyncEnumerable<AbstractCompetenceComponent> FetchComponents(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains)
public static async IAsyncEnumerable<AbstractCompetenceComponent> FetchComponents(IAsyncEnumerable<LearningDomainSubmission> submissions, IEnumerable<LearningDomain?> domains, IEnumerable<LearningDomainOutcome> outcomes)

Check warning on line 56 in Epsilon/Services/CompetenceDocumentService.cs

View workflow job for this annotation

GitHub Actions / Build .NET solution

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 56 in Epsilon/Services/CompetenceDocumentService.cs

View workflow job for this annotation

GitHub Actions / Unit testing

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
yield return new CompetenceProfileComponent(submissions, domains);
yield return new KpiTableComponent(submissions, domains);
yield return new CompetenceProfileComponent(submissions, domains, outcomes);
yield return new KpiTableComponent(submissions, domains, outcomes);
yield return new KpiMatrixComponent(submissions, domains, outcomes);
}
}

0 comments on commit bb59e9d

Please sign in to comment.