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

Consider holidays for next schoolday #1678

Merged
merged 8 commits into from
Jul 3, 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
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
Loading