Skip to content

Commit

Permalink
Add deleting custom grade types. (#1597)
Browse files Browse the repository at this point in the history
Add `GradeService.deleteCustomGradeType`:
```dart
  /// Deletes a custom grade type and removes it from all weight maps.
  ///
  /// A custom grade type can only be deleted if it is not assigned to any grade
  /// or as the final grade type of any term, otherwise a
  /// [GradeTypeStillAssignedException] is thrown.
  ///
  /// Throws [GradeTypeNotFoundException] if the grade type with the given [id]
  /// does not exist.
  ///
  /// Throws [ArgumentError] if the grade type with the given [id] is a
  /// predefined grade type.
  void deleteCustomGradeType(GradeTypeId id) {
```

Fixes #1532
  • Loading branch information
Jonas-Sander authored May 3, 2024
1 parent 0958128 commit d3298db
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 0 deletions.
55 changes: 55 additions & 0 deletions app/lib/grades/grades_service/grades_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,52 @@ class GradesService {
_updateState(newState);
}

/// Deletes a custom grade type and removes it from all weight maps.
///
/// A custom grade type can only be deleted if it is not assigned to any grade
/// or as the final grade type of any term, otherwise a
/// [GradeTypeStillAssignedException] is thrown.
///
/// Throws [GradeTypeNotFoundException] if the grade type with the given [id]
/// does not exist.
///
/// Throws [ArgumentError] if the grade type with the given [id] is a
/// predefined grade type.
void deleteCustomGradeType(GradeTypeId id) {
if (GradeType.predefinedGradeTypes.any((gt) => gt.id == id)) {
throw ArgumentError('Cannot delete a predefined grade type.');
}
if (!_hasGradeTypeWithId(id)) {
throw GradeTypeNotFoundException(id);
}

final anyGradeHasGradeTypeAssigned = _terms.any((term) => term.subjects
.expand((subj) => subj.grades)
.any((grade) => grade.gradeType == id));
if (anyGradeHasGradeTypeAssigned) {
throw GradeTypeStillAssignedException(id);
}

if (_terms.any((term) => term.finalGradeType == id)) {
throw GradeTypeStillAssignedException(id);
}

final newTerms = _terms.map((term) {
var newTerm = term.removeWeightingOfGradeType(id);
for (var subject in newTerm.subjects) {
subject = subject.removeGradeTypeWeight(id);
newTerm = newTerm.replaceSubject(subject);
}
return newTerm;
}).toIList();

final newCustomGrades = _customGradeTypes.removeWhere((gt) => gt.id == id);

final newState =
_state.copyWith(terms: newTerms, customGradeTypes: newCustomGrades);
_updateState(newState);
}

GradeType _getGradeType(GradeTypeId finalGradeType) {
return getPossibleGradeTypes().firstWhere((gt) => gt.id == finalGradeType);
}
Expand Down Expand Up @@ -473,6 +519,15 @@ class GradeTypeNotFoundException extends Equatable implements Exception {
List<Object?> get props => [id];
}

class GradeTypeStillAssignedException extends Equatable implements Exception {
final GradeTypeId id;

const GradeTypeStillAssignedException(this.id);

@override
List<Object?> get props => [id];
}

enum GradingSystem {
oneToSixWithPlusAndMinus(isNumericalAndContinous: true),
zeroToFifteenPoints(isNumericalAndContinous: true),
Expand Down
6 changes: 6 additions & 0 deletions app/lib/grades/grades_service/src/term.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ class TermModel extends Equatable {
return subjects.where((s) => s.id == id).first;
}

TermModel replaceSubject(SubjectModel subject) {
return _copyWith(
subjects: subjects.replaceAllWhere((s) => s.id == subject.id, subject),
);
}

TermModel _copyWith({
TermId? id,
IList<SubjectModel>? subjects,
Expand Down
234 changes: 234 additions & 0 deletions app/test/grades/grade_types_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ void main() {
final controller = GradesTestController();

final gradeTypes = controller.getPossibleGradeTypes();
expect(gradeTypes, GradeType.predefinedGradeTypes);

expect(
gradeTypes.map((element) => (
Expand Down Expand Up @@ -85,6 +86,239 @@ void main() {

expect(controller.getCustomGradeTypes().single.displayName, 'Foo');
});
test(
'Trying to delete an unknown custom grade type throws an $GradeTypeNotFoundException',
() {
final controller = GradesTestController();

expect(() => controller.deleteCustomGradeType(const GradeTypeId('foo')),
throwsA(const GradeTypeNotFoundException(GradeTypeId('foo'))));
});
test(
'A custom grade type should be deletable if it is still assigned in weight maps',
() {
final controller = GradesTestController();

controller.createTerm(
termWith(
id: const TermId('foo'),
gradeTypeWeights: {const GradeTypeId('foo'): const Weight.factor(2)},
subjects: [
subjectWith(
id: const SubjectId('bar'),
gradeTypeWeights: {
const GradeTypeId('foo'): const Weight.factor(2)
},
weightType: WeightType.perGradeType,
grades: [
// Added so that the subject will definitely be created
gradeWith(
id: const GradeId('bar'),
value: 3,
),
],
),
],
),
);

controller.deleteCustomGradeType(const GradeTypeId('foo'));

expect(
controller
.getCustomGradeTypes()
.where((element) => element.id == const GradeTypeId('foo'))
.toList(),
isEmpty);
});
test(
'If a custom grade type is deleted then it should be removed from all weight maps',
() {
final controller = GradesTestController();

controller.createTerm(termWith(
id: const TermId('foo'),
gradeTypeWeights: {const GradeTypeId('foo'): const Weight.factor(2)},
subjects: [
subjectWith(
id: const SubjectId('bar'),
gradeTypeWeights: {
const GradeTypeId('foo'): const Weight.factor(2)
},
weightType: WeightType.perGradeType,
grades: [
gradeWith(
id: const GradeId('bar'),
type: const GradeTypeId('foo'),
value: 3,
),
],
),
],
));

controller.deleteGrade(gradeId: const GradeId('bar'));
controller.deleteCustomGradeType(const GradeTypeId('foo'));

controller.createCustomGradeType(
const GradeType(id: GradeTypeId('foo'), displayName: 'foo'));
controller.addGrade(
termId: const TermId('foo'),
subjectId: const SubjectId('bar'),
value: gradeWith(value: 4, type: const GradeTypeId('foo')));
controller.addGrade(
termId: const TermId('foo'),
subjectId: const SubjectId('bar'),
value: gradeWith(value: 1, type: GradeType.other.id));

// If the grade type weights are really removed, then the calculated grade
// should reflect that.
// If the grade type weights would still be there, then the calculated
// grade would be 3, since the grade type weight for 'foo' is 2.
expect(
controller
.term(const TermId('foo'))
.subject(const SubjectId('bar'))
.calculatedGrade!
.asNum,
2.5);

// Change the weight type to inherit from term so that we can check that
// the grade type weight are removed from the term as well.
controller.changeWeightTypeForSubject(
termId: const TermId('foo'),
subjectId: const SubjectId('bar'),
weightType: WeightType.inheritFromTerm);

expect(
controller
.term(const TermId('foo'))
.subject(const SubjectId('bar'))
.calculatedGrade!
.asNum,
2.5);

expect(controller.term(const TermId('foo')).gradeTypeWeightings, isEmpty);
expect(
controller
.term(const TermId('foo'))
.subject(const SubjectId('bar'))
.gradeTypeWeights,
isEmpty);
});
test(
'Trying to delete a predefined grade type will throw an $ArgumentError',
() {
final controller = GradesTestController();

for (final gradeType in controller.getPossibleGradeTypes()) {
if (gradeType.predefinedType != null) {
expect(() => controller.deleteCustomGradeType(gradeType.id),
throwsA(isA<ArgumentError>()));
}
}
});
test(
'Trying to delete an unknown custom grade type that is still assigned as a final grade to a term throws an $GradeTypeStillAssignedException',
() {
final controller = GradesTestController();

controller.createTerm(
termWith(
id: const TermId('foo'),
finalGradeType: const GradeTypeId('foo'),
),
);

expect(() => controller.deleteCustomGradeType(const GradeTypeId('foo')),
throwsA(const GradeTypeStillAssignedException(GradeTypeId('foo'))));
});
test(
'Trying to delete an unknown custom grade type that is still assigned to a grade throws an $GradeTypeStillAssignedException',
() {
final controller = GradesTestController();

controller.createTerm(
termWith(
id: const TermId('foo'),
subjects: [
subjectWith(
grades: [
gradeWith(
type: const GradeTypeId('foo'),
value: 3,
)
],
),
],
),
);

expect(() => controller.deleteCustomGradeType(const GradeTypeId('foo')),
throwsA(const GradeTypeStillAssignedException(GradeTypeId('foo'))));
});
test(
'A custom grade type can be deleted if it is not assigned to anything (simple case)',
() {
final controller = GradesTestController();
controller.createCustomGradeType(
const GradeType(id: GradeTypeId('foo'), displayName: 'Foo'));
controller.deleteCustomGradeType(const GradeTypeId('foo'));

expect(controller.getCustomGradeTypes(), isEmpty);
});
test('A custom grade type can be deleted if it is not assigned to anything',
() {
final controller = GradesTestController();

const termId = TermId('foo');
controller.createTerm(termWith(
id: termId,
finalGradeType: const GradeTypeId('foo'),
gradeTypeWeights: {const GradeTypeId('foo'): const Weight.factor(2)},
subjects: [
subjectWith(
id: const SubjectId('bar'),
finalGradeType: const GradeTypeId('foo'),
weightType: WeightType.perGradeType,
gradeTypeWeights: {
const GradeTypeId('foo'): const Weight.factor(2)
},
grades: [
gradeWith(
id: const GradeId('bar'),
type: const GradeTypeId('foo'),
value: 3,
),
],
),
],
));

controller.changeFinalGradeTypeForSubject(
termId: const TermId('foo'),
subjectId: const SubjectId('bar'),
gradeType: GradeType.other.id,
);
controller.changeFinalGradeTypeForTerm(
termId: termId,
gradeTypeId: GradeType.other.id,
);
controller.removeWeightTypeForSubject(
termId: termId,
subjectId: const SubjectId('bar'),
gradeTypeId: const GradeTypeId('foo'),
);
controller.removeGradeTypeWeightForTerm(
termId: termId,
gradeTypeId: const GradeTypeId('foo'),
);

controller.deleteGrade(gradeId: const GradeId('bar'));
controller.deleteCustomGradeType(const GradeTypeId('foo'));

expect(controller.getCustomGradeTypes(), isEmpty);
});
test(
'Trying to add a grade with an non-existing gradeType will cause an UnknownGradeTypeException',
() {
Expand Down
13 changes: 13 additions & 0 deletions app/test/grades/grades_test_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ class GradesTestController {
design: subject.design,
connectedCourses: subject.connectedCourses,
));

if (subject.grades.isNotEmpty) {
throw ArgumentError('Use addGrade to add grades to a subject');
}
}

IList<TestSubject> getSubjects() {
Expand Down Expand Up @@ -294,6 +298,15 @@ class GradesTestController {
void deleteGrade({required GradeId gradeId}) {
service.deleteGrade(gradeId);
}

void deleteCustomGradeType(GradeTypeId gradeTypeId) {
service.deleteCustomGradeType(gradeTypeId);
}

void changeFinalGradeTypeForTerm(
{required TermId termId, required GradeTypeId gradeTypeId}) {
service.editTerm(id: termId, finalGradeType: gradeTypeId);
}
}

TestTerm termWith({
Expand Down

0 comments on commit d3298db

Please sign in to comment.