Skip to content

Commit

Permalink
Consider holidays for next schoolday (#1678)
Browse files Browse the repository at this point in the history
Fixes #1179

---------

Co-authored-by: Jonas Sander <[email protected]>
  • Loading branch information
ImGxrke and Jonas-Sander authored Jul 3, 2024
1 parent 862d7fc commit 9dd1f42
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 38 deletions.
23 changes: 16 additions & 7 deletions app/lib/homework/homework_dialog/homework_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import 'package:sharezone/sharezone_plus/page/sharezone_plus_page.dart';
import 'package:sharezone/sharezone_plus/subscription_service/subscription_service.dart';
import 'package:sharezone/timetable/src/edit_time.dart';
import 'package:sharezone/util/next_lesson_calculator/next_lesson_calculator.dart';
import 'package:sharezone/util/next_schoolday_calculator/next_schoolday_calculator.dart';
import 'package:sharezone/widgets/material/save_button.dart';
import 'package:sharezone_widgets/sharezone_widgets.dart';
import 'package:time/time.dart';
Expand All @@ -46,6 +47,7 @@ class HomeworkDialog extends StatefulWidget {
required this.id,
this.homeworkDialogApi,
this.nextLessonCalculator,
this.nextSchooldayCalculator,
this.showDueDateSelectionChips = true,
});

Expand All @@ -54,6 +56,7 @@ class HomeworkDialog extends StatefulWidget {
final HomeworkId? id;
final HomeworkDialogApi? homeworkDialogApi;
final NextLessonCalculator? nextLessonCalculator;
final NextSchooldayCalculator? nextSchooldayCalculator;
final bool showDueDateSelectionChips;

@override
Expand All @@ -70,23 +73,29 @@ class _HomeworkDialogState extends State<HomeworkDialog> {
final markdownAnalytics = BlocProvider.of<MarkdownAnalytics>(context);
final szContext = BlocProvider.of<SharezoneContext>(context);
final analytics = szContext.analytics;
final enabledWeekDays = szContext
.api.user.data!.userSettings.enabledWeekDays
.getEnabledWeekDaysList();
final holidayManager = BlocProvider.of<HolidayBloc>(context).holidayManager;

late NextLessonCalculator nextLessonCalculator;
if (widget.nextLessonCalculator != null) {
nextLessonCalculator = widget.nextLessonCalculator!;
} else {
final holidayManager =
BlocProvider.of<HolidayBloc>(context).holidayManager;
nextLessonCalculator = NextLessonCalculator(
timetableGateway: szContext.api.timetable,
userGateway: szContext.api.user,
holidayManager: holidayManager,
);
}

late NextSchooldayCalculator nextSchooldayCalculator;
if (widget.nextSchooldayCalculator != null) {
nextSchooldayCalculator = widget.nextSchooldayCalculator!;
} else {
nextSchooldayCalculator = NextSchooldayCalculator(
userGateway: szContext.api.user,
holidayManager: holidayManager,
);
}

if (widget.id != null) {
homework = szContext.api.homework
.singleHomework(widget.id!.value, source: Source.cache)
Expand All @@ -95,7 +104,7 @@ class _HomeworkDialogState extends State<HomeworkDialog> {
homeworkId: widget.id,
api: widget.homeworkDialogApi ?? HomeworkDialogApi(szContext.api),
nextLessonCalculator: nextLessonCalculator,
enabledWeekdays: enabledWeekDays,
nextSchooldayCalculator: nextSchooldayCalculator,
markdownAnalytics: markdownAnalytics,
analytics: analytics,
);
Expand All @@ -106,7 +115,7 @@ class _HomeworkDialogState extends State<HomeworkDialog> {
bloc = HomeworkDialogBloc(
api: widget.homeworkDialogApi ?? HomeworkDialogApi(szContext.api),
nextLessonCalculator: nextLessonCalculator,
enabledWeekdays: enabledWeekDays,
nextSchooldayCalculator: nextSchooldayCalculator,
markdownAnalytics: markdownAnalytics,
analytics: analytics,
);
Expand Down
37 changes: 14 additions & 23 deletions app/lib/homework/homework_dialog/homework_dialog_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import 'package:clock/clock.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:common_domain_models/common_domain_models.dart';
import 'package:date/date.dart';
import 'package:date/weekday.dart';
import 'package:equatable/equatable.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:files_basics/files_models.dart';
Expand All @@ -25,6 +24,7 @@ import 'package:meta/meta.dart';
import 'package:sharezone/markdown/markdown_analytics.dart';
import 'package:sharezone/util/api.dart';
import 'package:sharezone/util/next_lesson_calculator/next_lesson_calculator.dart';
import 'package:sharezone/util/next_schoolday_calculator/next_schoolday_calculator.dart';
import 'package:time/time.dart';
import 'package:user/user.dart';

Expand Down Expand Up @@ -455,11 +455,10 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
final Analytics analytics;
final MarkdownAnalytics markdownAnalytics;
final NextLessonCalculator nextLessonCalculator;
final Clock _clock;
final NextSchooldayCalculator nextSchooldayCalculator;
HomeworkDto? _initialHomework;
late final IList<CloudFile> _initialAttachments;
late final bool isEditing;
final List<WeekDay> enabledWeekdays;

_DateSelection _initialDateSelection = _DateSelection.noSelection;
_DateSelection _dateSelection = _DateSelection.noSelection;
Expand Down Expand Up @@ -497,11 +496,10 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
required this.nextLessonCalculator,
required this.analytics,
required this.markdownAnalytics,
required this.enabledWeekdays,
required this.nextSchooldayCalculator,
Clock? clockOverride,
HomeworkId? homeworkId,
}) : _clock = clockOverride ?? clock,
super(homeworkId != null
}) : super(homeworkId != null
? LoadingHomework(homeworkId, isEditing: true)
: emptyCreateHomeworkDialogState) {
isEditing = homeworkId != null;
Expand Down Expand Up @@ -649,7 +647,9 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
(event, emit) async {
switch (event.newDueDate) {
case DateDueDateSelection s:
if (s.date == _getNextSchoolday()) {
final nextSchoolDay =
await nextSchooldayCalculator.tryCalculateNextSchoolday();
if (s.date == nextSchoolDay) {
_dateSelection = _dateSelection.copyWith(
dueDate: s.date,
dueDateSelection: const NextSchooldayDueDateSelection(),
Expand All @@ -660,8 +660,10 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
_dateSelection.copyWith(dueDate: s.date, dueDateSelection: s);
break;
case NextSchooldayDueDateSelection s:
final nextSchoolDay =
await nextSchooldayCalculator.tryCalculateNextSchoolday();
_dateSelection = _dateSelection.copyWith(
dueDate: _getNextSchoolday(),
dueDate: nextSchoolDay,
dueDateSelection: s,
);
break;
Expand Down Expand Up @@ -705,6 +707,9 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
.tryCalculateXNextLesson(course.id, inLessons: inXLessons);
_hasLessons[course.id] = newLessonDate != null;

final nextSchoolDay =
await nextSchooldayCalculator.tryCalculateNextSchoolday();

// Manual date was already set, we don't want to overwrite it.
if (_dateSelection.dueDateSelection != null &&
_dateSelection.dueDateSelection is! InXLessonsDueDateSelection) {
Expand Down Expand Up @@ -732,7 +737,7 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
}

_dateSelection = _dateSelection.copyWith(
dueDate: _getNextSchoolday(),
dueDate: nextSchoolDay,
dueDateSelection: const NextSchooldayDueDateSelection(),
);
emit(_getNewState());
Expand Down Expand Up @@ -796,20 +801,6 @@ class HomeworkDialogBloc extends Bloc<HomeworkDialogEvent, HomeworkDialogState>
);
}

Date _getNextSchoolday() {
var candidate = _clock.now().toDate();
// hope this code is refactored by then 🤡
while (candidate.year < 2050) {
candidate = candidate.addDays(1);
if (enabledWeekdays.contains(candidate.weekDayEnum)) {
return candidate;
}
}

// Should never happen, but who knows ¯\_(ツ)_/¯
return _clock.now().toDate().addDays(1);
}

Ready _getNewState() {
final didHomeworkChange = isEditing
? _initialHomework != _homework
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) 2022 Sharezone UG (haftungsbeschränkt)
// Licensed under the EUPL-1.2-or-later.
//
// You may obtain a copy of the Licence at:
// https://joinup.ec.europa.eu/software/page/eupl
//
// SPDX-License-Identifier: EUPL-1.2

import 'dart:developer';

import 'package:date/date.dart';
import 'package:holidays/holidays.dart';
import 'package:sharezone/holidays/holiday_bloc.dart';
import 'package:sharezone/util/api/user_api.dart';
import 'package:user/user.dart';

class NextSchooldayCalculator {
final UserGateway _userGateway;
final HolidayService _holidayManager;

NextSchooldayCalculator({
required UserGateway userGateway,
required HolidayService holidayManager,
}) : _userGateway = userGateway,
_holidayManager = holidayManager;

Future<Date?> tryCalculateNextSchoolday() async {
return tryCalculateXNextSchoolday(inSchooldays: 1);
}

Future<Date?> tryCalculateXNextSchoolday({int inSchooldays = 1}) async {
assert(inSchooldays > 0);
try {
final user = await _userGateway.get();
final enabledWeekdays = user.userSettings.enabledWeekDays;
final holidays = await _tryLoadHolidays(user);
final results = _NextSchooldayCaluclation(enabledWeekdays, holidays)
.calculate(days: inSchooldays);
if (results.isEmpty) return Date.today().addDays(1);
return results.elementAt(inSchooldays - 1);
} catch (e, s) {
log('Could not calculate next schoolday: $e\n$s',
error: e, stackTrace: s);
return null;
}
}

Future<List<Holiday?>> _tryLoadHolidays(AppUser user) async {
try {
return await _holidayManager.load(toStateOrThrow(user.state));
} catch (e, s) {
log('Could not load holidays for calculating next schooldays: $e',
error: e, stackTrace: s);
return [];
}
}
}

class _NextSchooldayCaluclation {
final EnabledWeekDays enabledWeekdays;
final List<Holiday?> holidays;

_NextSchooldayCaluclation(this.enabledWeekdays, this.holidays);

List<Date> calculate({int days = 3}) {
if (enabledWeekdays.getEnabledWeekDaysList().isEmpty) return [];
List<Date> results = [];
Date date = Date.today();
while (results.length < days) {
// LOOP TO NEXT DAY
date = date.addDays(1);
// CHECKS IF IS HOLIDAY
if (_isHolidayAt(date)) continue;
if (!_isSchooldayAt(date)) continue;
// ADDS DATE TO RESULTS
results.add(date);
}
return results;
}

bool _isSchooldayAt(Date date) {
return enabledWeekdays.getEnabledWeekDaysList().contains(date.weekDayEnum);
}

bool _isHolidayAt(Date date) {
for (final holiday in holidays) {
Date start = Date.fromDateTime(holiday!.start);
Date end = Date.fromDateTime(holiday.end);
if (date.isInsideDateRange(start, end)) return true;
}
return false;
}
}
20 changes: 12 additions & 8 deletions app/test/homework/homework_dialog_bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import 'package:rxdart/rxdart.dart';
import 'package:sharezone/homework/homework_dialog/homework_dialog_bloc.dart';
import 'package:sharezone/markdown/markdown_analytics.dart';
import 'package:time/time.dart';
import 'package:user/user.dart';

import '../analytics/analytics_test.dart';
import 'homework_dialog_test.dart';
Expand All @@ -34,13 +33,15 @@ void main() {
late MockCourseGateway courseGateway;
late MockHomeworkDialogApi homeworkDialogApi;
late MockNextLessonCalculator nextLessonCalculator;
late MockNextSchooldayCalculator nextSchooldayCalculator;
late LocalAnalyticsBackend analyticsBackend;
late Analytics analytics;

setUp(() {
courseGateway = MockCourseGateway();
homeworkDialogApi = MockHomeworkDialogApi();
nextLessonCalculator = MockNextLessonCalculator();
nextSchooldayCalculator = MockNextSchooldayCalculator();
analyticsBackend = LocalAnalyticsBackend();
analytics = Analytics(analyticsBackend);
});
Expand All @@ -50,20 +51,20 @@ void main() {
api: homeworkDialogApi,
clockOverride: clock,
nextLessonCalculator: nextLessonCalculator,
nextSchooldayCalculator: nextSchooldayCalculator,
analytics: analytics,
markdownAnalytics: MarkdownAnalytics(analytics),
enabledWeekdays: EnabledWeekDays.standard.getEnabledWeekDaysList(),
);
}

HomeworkDialogBloc createBlocForEditingHomeworkDialog(HomeworkId id) {
return HomeworkDialogBloc(
api: homeworkDialogApi,
nextLessonCalculator: nextLessonCalculator,
nextSchooldayCalculator: nextSchooldayCalculator,
analytics: analytics,
homeworkId: id,
markdownAnalytics: MarkdownAnalytics(analytics),
enabledWeekdays: EnabledWeekDays.standard.getEnabledWeekDaysList(),
);
}

Expand All @@ -84,13 +85,15 @@ void main() {
nextLessonCalculator.dateToReturn = null;
final testClock = Clock.fixed(Date.parse(currentDate).toDateTime);
addCourse(courseWith(id: 'foo'));
final bloc = createBlocForNewHomeworkDialog(clock: testClock);

bloc.add(const CourseChanged(CourseId('foo')));
await pumpEventQueue();
await withClock(testClock, () async {
final bloc = createBlocForNewHomeworkDialog(clock: testClock);
bloc.add(const CourseChanged(CourseId('foo')));
await pumpEventQueue();

final state = bloc.state as Ready;
expect(state.dueDate.$1, Date.parse(expectedLessonDate));
final state = bloc.state as Ready;
expect(state.dueDate.$1, Date.parse(expectedLessonDate));
});
}

// | Current date | Next lesson date |
Expand Down Expand Up @@ -641,6 +644,7 @@ void main() {
bloc.add(const TitleChanged('abc'));
bloc.add(const CourseChanged(CourseId('foo_course')));
bloc.add(DueDateChanged(DueDateSelection.date(Date('2024-03-08'))));
await pumpEventQueue(); // Wait for the due date to be checked for next schoolday
bloc.add(const Save());
await bloc.stream.whereType<SavedSuccessfully>().first;

Expand Down
Loading

0 comments on commit 9dd1f42

Please sign in to comment.