From 6d0b53f0349c4c5d43e992e09bdfd666f7bdb907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 22 Apr 2022 10:04:01 +0200 Subject: [PATCH 1/3] MOBILE-3881 timeline: Fix minimim time to get events from --- .../block/timeline/components/events/events.ts | 6 +++--- src/addons/block/timeline/services/timeline.ts | 16 +++++++++++++--- src/core/singletons/time.ts | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/addons/block/timeline/components/events/events.ts b/src/addons/block/timeline/components/events/events.ts index 4e0155873..fe986201f 100644 --- a/src/addons/block/timeline/components/events/events.ts +++ b/src/addons/block/timeline/components/events/events.ts @@ -18,10 +18,10 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreCourse } from '@features/course/services/course'; -import moment from 'moment'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { AddonCalendarEvent } from '@addons/calendar/services/calendar'; import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; +import { AddonBlockTimeline } from '../../services/timeline'; /** * Directive to render a list of events in course overview. @@ -94,8 +94,8 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { * @return Filtered events. */ protected async filterEventsByTime(start: number, end?: number): Promise { - start = moment().add(start, 'days').startOf('day').unix(); - end = end !== undefined ? moment().add(end, 'days').startOf('day').unix() : end; + start = AddonBlockTimeline.getDayStart(start); + end = end !== undefined ? AddonBlockTimeline.getDayStart(end) : end; return await Promise.all(this.events.filter((event) => { if (end) { diff --git a/src/addons/block/timeline/services/timeline.ts b/src/addons/block/timeline/services/timeline.ts index 831c5c5c5..c37f36591 100644 --- a/src/addons/block/timeline/services/timeline.ts +++ b/src/addons/block/timeline/services/timeline.ts @@ -55,7 +55,7 @@ export class AddonBlockTimelineProvider { ): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> { const site = await CoreSites.getSite(siteId); - const time = moment().subtract(14, 'days').unix(); // Check two weeks ago. + const time = this.getDayStart(-14); // Check two weeks ago. const data: AddonCalendarGetActionEventsByCourseWSParams = { timesortfrom: time, @@ -109,7 +109,7 @@ export class AddonBlockTimelineProvider { ): Promise<{[courseId: string]: { events: AddonCalendarEvent[]; canLoadMore?: number } }> { const site = await CoreSites.getSite(siteId); - const time = moment().subtract(14, 'days').unix(); // Check two weeks ago. + const time = this.getDayStart(-14); // Check two weeks ago. const data: AddonCalendarGetActionEventsByCoursesWSParams = { timesortfrom: time, @@ -164,7 +164,7 @@ export class AddonBlockTimelineProvider { ): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> { const site = await CoreSites.getSite(siteId); - const timesortfrom = moment().subtract(14, 'days').unix(); // Check two weeks ago. + const timesortfrom = this.getDayStart(-14); // Check two weeks ago. const limitnum = AddonBlockTimelineProvider.EVENTS_LIMIT; const data: AddonCalendarGetActionEventsByTimesortWSParams = { @@ -275,6 +275,16 @@ export class AddonBlockTimelineProvider { }; } + /** + * Returns the timestamp at the start of the day with an optional offset. + * + * @param daysOffset Offset days to add or substract. + * @return timestamp. + */ + getDayStart(daysOffset = 0): number { + return moment().startOf('day').add(daysOffset, 'days').unix(); + } + } export const AddonBlockTimeline = makeSingleton(AddonBlockTimelineProvider); diff --git a/src/core/singletons/time.ts b/src/core/singletons/time.ts index 6b222937b..5c95ed837 100644 --- a/src/core/singletons/time.ts +++ b/src/core/singletons/time.ts @@ -81,7 +81,7 @@ export class CoreTime { /** * Converts a number of seconds into a short human readable format: minutes and seconds, in fromat: 3' 27''. * - * @param seconds Seconds + * @param duration Duration in seconds. * @return Short human readable text. */ static formatTimeShort(duration: number): string { From f9b19513472057aa04c056f97cb953ba923797dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 22 Apr 2022 11:17:49 +0200 Subject: [PATCH 2/3] MOBILE-3881 timeline: Fix overdue tasks showing different from LMS --- .../timeline/components/events/events.ts | 43 ++++++++++++------- .../timeline/addon-block-timeline.html | 4 +- .../timeline/components/timeline/timeline.ts | 2 + src/addons/calendar/services/calendar.ts | 2 +- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/addons/block/timeline/components/events/events.ts b/src/addons/block/timeline/components/events/events.ts index fe986201f..653313e31 100644 --- a/src/addons/block/timeline/components/events/events.ts +++ b/src/addons/block/timeline/components/events/events.ts @@ -37,6 +37,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { @Input() course?: CoreEnrolledCourseDataWithOptions; // Whether to show the course name. @Input() from = 0; // Number of days from today to offset the events. @Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit. + @Input() overdue = false; // If filtering overdue events or not. @Input() canLoadMore = false; // Whether more events can be loaded. @Output() loadMore = new EventEmitter(); // Notify that more events should be loaded. @@ -53,18 +54,13 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { if (changes.events || changes.from || changes.to) { if (this.events) { - const filteredEvents = await this.filterEventsByTime(this.from, this.to); + const filteredEvents = await this.filterEventsByTime(); this.empty = !filteredEvents || filteredEvents.length <= 0; - const now = CoreTimeUtils.timestamp(); - - const eventsByDay: Record = {}; + const eventsByDay: Record = {}; filteredEvents.forEach((event) => { const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort); - // Already calculated on 4.0 onwards but this will be live. - event.overdue = event.timesort < now; - if (eventsByDay[dayTimestamp]) { eventsByDay[dayTimestamp].push(event); } else { @@ -89,20 +85,34 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { /** * Filter the events by time. * - * @param start Number of days to start getting events from today. E.g. -1 will get events from yesterday. - * @param end Number of days after the start. * @return Filtered events. */ - protected async filterEventsByTime(start: number, end?: number): Promise { - start = AddonBlockTimeline.getDayStart(start); - end = end !== undefined ? AddonBlockTimeline.getDayStart(end) : end; + protected async filterEventsByTime(): Promise { + const start = AddonBlockTimeline.getDayStart(this.from); + const end = this.to !== undefined + ? AddonBlockTimeline.getDayStart(this.to) + : undefined; + + const now = CoreTimeUtils.timestamp(); + const midnight = AddonBlockTimeline.getDayStart(); return await Promise.all(this.events.filter((event) => { - if (end) { - return start <= event.timesort && event.timesort < end; + if (start > event.timesort || (end && event.timesort >= end)) { + return false; } - return start <= event.timesort; + // Already calculated on 4.0 onwards but this will be live. + event.overdue = event.timesort < now; + + if (event.eventtype === 'open' || event.eventtype === 'opensubmission') { + const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort); + + return dayTimestamp > midnight; + } + + // When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue. + // This means if filtering by overdue, some events fetched might not be required (eg if due later today). + return (!this.overdue || event.overdue); }).map(async (event) => { event.iconUrl = await CoreCourse.getModuleIconSrc(event.icon.component); event.modulename = event.modulename || event.icon.component; @@ -147,7 +157,8 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { } -type AddonBlockTimelineEvent = AddonCalendarEvent & { +type AddonBlockTimelineEvent = Omit & { + eventtype: string; iconUrl?: string; iconTitle?: string; }; diff --git a/src/addons/block/timeline/components/timeline/addon-block-timeline.html b/src/addons/block/timeline/components/timeline/addon-block-timeline.html index c494a8f6f..e518b440c 100644 --- a/src/addons/block/timeline/components/timeline/addon-block-timeline.html +++ b/src/addons/block/timeline/components/timeline/addon-block-timeline.html @@ -60,12 +60,12 @@ + [from]="dataFrom" [to]="dataTo" [overdue]="overdue"> + [course]="course" [from]="dataFrom" [to]="dataTo" [overdue]="overdue"> diff --git a/src/addons/block/timeline/components/timeline/timeline.ts b/src/addons/block/timeline/components/timeline/timeline.ts index e26045dd8..dc23ebbca 100644 --- a/src/addons/block/timeline/components/timeline/timeline.ts +++ b/src/addons/block/timeline/components/timeline/timeline.ts @@ -58,6 +58,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen dataFrom?: number; dataTo?: number; + overdue = false; searchEnabled = false; searchText = ''; @@ -200,6 +201,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen switchFilter(filter: string): void { this.filter = filter; this.currentSite.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter); + this.overdue = this.filter === 'overdue'; switch (this.filter) { case 'overdue': diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index c8bbf90d3..6877b54c3 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -45,7 +45,7 @@ import { CoreText } from '@singletons/text'; const ROOT_CACHE_KEY = 'mmaCalendar:'; /** - * Context levels enumeration. + * Main calendar Event types enumeration. */ export enum AddonCalendarEventType { SITE = 'site', From 0f5416e2f0750da7aa5c015ffcfb2d12fd87ae32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 22 Apr 2022 12:12:41 +0200 Subject: [PATCH 3/3] MOBILE-3881 timeline: Only show in progress courses --- .../components/myoverview/myoverview.ts | 51 ++++--------------- .../timeline/components/timeline/timeline.ts | 27 +++++++--- .../courses/services/courses-helper.ts | 46 +++++++++++++++++ 3 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts index 14bb2c6ff..8ca110b87 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addons/block/myoverview/components/myoverview/myoverview.ts @@ -27,7 +27,6 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion'; import { IonSearchbar } from '@ionic/angular'; -import moment from 'moment'; import { CoreNavigator } from '@services/navigator'; const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] = @@ -478,13 +477,19 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem break; case 'inprogress': this.filteredCourses = this.filteredCourses.filter((course) => - !course.hidden && !this.isPastCourse(course) && !this.isFutureCourse(course)); + !course.hidden && + !CoreCoursesHelper.isPastCourse(course, this.gradePeriodAfter) && + !CoreCoursesHelper.isFutureCourse(course, this.gradePeriodAfter, this.gradePeriodBefore)); break; case 'future': - this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden && this.isFutureCourse(course)); + this.filteredCourses = this.filteredCourses.filter((course) => + !course.hidden && + CoreCoursesHelper.isFutureCourse(course, this.gradePeriodAfter, this.gradePeriodBefore)); break; case 'past': - this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden && this.isPastCourse(course)); + this.filteredCourses = this.filteredCourses.filter((course) => + !course.hidden && + CoreCoursesHelper.isPastCourse(course, this.gradePeriodAfter)); break; case 'favourite': this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden && course.isfavourite); @@ -515,44 +520,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.initPrefetchCoursesIcons(); } - /** - * Calculates if course date is past. - * - * @param course Course Object. - * @return Wether the course is past. - */ - protected isPastCourse(course: CoreEnrolledCourseDataWithOptions): boolean { - if (course.completed) { - return true; - } - - if (!course.enddate) { - return false; - } - - // Calculate the end date to use for display classification purposes, incorporating the grace period, if any. - const endDate = moment(course.enddate * 1000).add(this.gradePeriodAfter, 'days').valueOf(); - - return endDate < this.today; - } - - /** - * Calculates if course date is future. - * - * @param course Course Object. - * @return Wether the course is future. - */ - protected isFutureCourse(course: CoreEnrolledCourseDataWithOptions): boolean { - if (this.isPastCourse(course) || !course.startdate) { - return false; - } - - // Calculate the start date to use for display classification purposes, incorporating the grace period, if any. - const startDate = moment(course.startdate * 1000).subtract(this.gradePeriodBefore, 'days').valueOf(); - - return startDate > this.today; - } - /** * Sort courses * diff --git a/src/addons/block/timeline/components/timeline/timeline.ts b/src/addons/block/timeline/components/timeline/timeline.ts index dc23ebbca..d0602b990 100644 --- a/src/addons/block/timeline/components/timeline/timeline.ts +++ b/src/addons/block/timeline/components/timeline/timeline.ts @@ -63,8 +63,10 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen searchEnabled = false; searchText = ''; - protected courseIds: number[] = []; + protected courseIdsToInvalidate: number[] = []; protected fetchContentDefaultError = 'Error getting timeline data.'; + protected gradePeriodAfter = 0; + protected gradePeriodBefore = 0; constructor() { super('AddonBlockTimelineComponent'); @@ -106,8 +108,8 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen promises.push(AddonBlockTimeline.invalidateActionEventsByCourses()); promises.push(CoreCourses.invalidateUserCourses()); promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); - if (this.courseIds.length > 0) { - promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(','))); + if (this.courseIdsToInvalidate.length > 0) { + promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIdsToInvalidate.join(','))); } return CoreUtils.allPromises(promises); @@ -172,13 +174,26 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen * @return Promise resolved when done. */ protected async fetchMyOverviewTimelineByCourses(): Promise { + try { + this.gradePeriodAfter = parseInt(await this.currentSite.getConfig('coursegraceperiodafter'), 10); + this.gradePeriodBefore = parseInt(await this.currentSite.getConfig('coursegraceperiodbefore'), 10); + } catch { + this.gradePeriodAfter = 0; + this.gradePeriodBefore = 0; + } + // Do not filter courses by date because they can contain activities due. this.timelineCourses.courses = await CoreCoursesHelper.getUserCoursesWithOptions(); + this.courseIdsToInvalidate = this.timelineCourses.courses.map((course) => course.id); + + // Filter only in progress courses. + this.timelineCourses.courses = this.timelineCourses.courses.filter((course) => + !course.hidden && + !CoreCoursesHelper.isPastCourse(course, this.gradePeriodAfter) && + !CoreCoursesHelper.isFutureCourse(course, this.gradePeriodAfter, this.gradePeriodBefore)); if (this.timelineCourses.courses.length > 0) { - this.courseIds = this.timelineCourses.courses.map((course) => course.id); - - const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(this.courseIds, this.searchText); + const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(this.courseIdsToInvalidate, this.searchText); this.timelineCourses.courses = this.timelineCourses.courses.filter((course) => { if (courseEvents[course.id].events.length == 0) { diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index cb0e7b1f4..688db4f43 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -25,6 +25,7 @@ import { import { makeSingleton, Translate } from '@singletons'; import { CoreWSExternalFile } from '@services/ws'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion'; +import moment from 'moment'; /** * Helper to gather some common courses functions. @@ -293,6 +294,51 @@ export class CoreCoursesHelperProvider { })); } + /** + * Calculates if course date is past. + * + * @param course Course Object. + * @param gradePeriodAfter Classify past courses as in progress for these many days after the course end date. + * @return Wether the course is past. + */ + isPastCourse(course: CoreEnrolledCourseDataWithOptions, gradePeriodAfter = 0): boolean { + if (course.completed) { + return true; + } + + if (!course.enddate) { + return false; + } + + // Calculate the end date to use for display classification purposes, incorporating the grace period, if any. + const endDate = moment(course.enddate * 1000).add(gradePeriodAfter, 'days').valueOf(); + + return endDate < Date.now(); + } + + /** + * Calculates if course date is future. + * + * @param course Course Object. + * @param gradePeriodAfter Classify past courses as in progress for these many days after the course end date. + * @param gradePeriodBefore Classify future courses as in progress for these many days prior to the course start date. + * @return Wether the course is future. + */ + isFutureCourse( + course: CoreEnrolledCourseDataWithOptions, + gradePeriodAfter = 0, + gradePeriodBefore = 0, + ): boolean { + if (this.isPastCourse(course, gradePeriodAfter) || !course.startdate) { + return false; + } + + // Calculate the start date to use for display classification purposes, incorporating the grace period, if any. + const startDate = moment(course.startdate * 1000).subtract(gradePeriodBefore, 'days').valueOf(); + + return startDate > Date.now(); + } + } export const CoreCoursesHelper = makeSingleton(CoreCoursesHelperProvider);