forked from EVOgeek/Vmeda.Online
470 lines
18 KiB
TypeScript
470 lines
18 KiB
TypeScript
// (C) Copyright 2015 Martin Dougiamas
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { CoreLoggerProvider } from '@providers/logger';
|
|
import { CoreSitesProvider } from '@providers/sites';
|
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
|
import { AddonCalendarProvider } from './calendar';
|
|
import { CoreConstants } from '@core/constants';
|
|
import { CoreConfigProvider } from '@providers/config';
|
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
|
import * as moment from 'moment';
|
|
|
|
/**
|
|
* Service that provides some features regarding lists of courses and categories.
|
|
*/
|
|
@Injectable()
|
|
export class AddonCalendarHelperProvider {
|
|
protected logger;
|
|
|
|
protected EVENTICONS = {
|
|
course: 'fa-university',
|
|
group: 'people',
|
|
site: 'globe',
|
|
user: 'person',
|
|
category: 'fa-cubes'
|
|
};
|
|
|
|
constructor(logger: CoreLoggerProvider,
|
|
private courseProvider: CoreCourseProvider,
|
|
private sitesProvider: CoreSitesProvider,
|
|
private calendarProvider: AddonCalendarProvider,
|
|
private configProvider: CoreConfigProvider,
|
|
private utils: CoreUtilsProvider) {
|
|
this.logger = logger.getInstance('AddonCalendarHelperProvider');
|
|
}
|
|
|
|
/**
|
|
* Calculate some day data based on a list of events for that day.
|
|
*
|
|
* @param day Day.
|
|
* @param events Events.
|
|
*/
|
|
calculateDayData(day: any, events: any[]): void {
|
|
day.hasevents = events.length > 0;
|
|
day.haslastdayofevent = false;
|
|
|
|
const types = {};
|
|
events.forEach((event) => {
|
|
types[event.formattedType || event.eventtype] = true;
|
|
|
|
if (event.islastday) {
|
|
day.haslastdayofevent = true;
|
|
}
|
|
});
|
|
|
|
day.calendareventtypes = Object.keys(types);
|
|
}
|
|
|
|
/**
|
|
* Check if current user can create/edit events.
|
|
*
|
|
* @param courseId Course ID. If not defined, site calendar.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with boolean: whether the user can create events.
|
|
*/
|
|
canEditEvents(courseId?: number, siteId?: string): Promise<boolean> {
|
|
return this.calendarProvider.canEditEvents(siteId).then((canEdit) => {
|
|
if (!canEdit) {
|
|
return false;
|
|
}
|
|
|
|
// Site allows creating events. Check if the user has permissions to do so.
|
|
return this.calendarProvider.getAllowedEventTypes(courseId, siteId).then((types) => {
|
|
return Object.keys(types).length > 0;
|
|
});
|
|
}).catch(() => {
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Classify events into their respective months and days. If an event duration covers more than one day,
|
|
* it will be included in all the days it lasts.
|
|
*
|
|
* @param events Events to classify.
|
|
* @return Object with the classified events.
|
|
*/
|
|
classifyIntoMonths(events: any[]): {[monthId: string]: {[day: number]: any[]}} {
|
|
|
|
const result = {};
|
|
|
|
events.forEach((event) => {
|
|
const treatedDay = moment(new Date(event.timestart * 1000)),
|
|
endDay = moment(new Date((event.timestart + (event.timeduration || 0)) * 1000));
|
|
|
|
// Add the event to all the days it lasts.
|
|
while (!treatedDay.isAfter(endDay, 'day')) {
|
|
const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1),
|
|
day = treatedDay.date();
|
|
|
|
if (!result[monthId]) {
|
|
result[monthId] = {};
|
|
}
|
|
if (!result[monthId][day]) {
|
|
result[monthId][day] = [];
|
|
}
|
|
result[monthId][day].push(event);
|
|
|
|
treatedDay.add(1, 'day'); // Treat next day.
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Convenience function to format some event data to be rendered.
|
|
*
|
|
* @param e Event to format.
|
|
*/
|
|
formatEventData(e: any): void {
|
|
e.icon = this.EVENTICONS[e.eventtype] || false;
|
|
if (!e.icon) {
|
|
e.icon = this.courseProvider.getModuleIconSrc(e.modulename);
|
|
e.moduleIcon = e.icon;
|
|
}
|
|
|
|
e.formattedType = this.calendarProvider.getEventType(e);
|
|
|
|
if (typeof e.duration != 'undefined') {
|
|
// It's an offline event, add some calculated data.
|
|
e.format = 1;
|
|
e.visible = 1;
|
|
|
|
if (e.duration == 1) {
|
|
e.timeduration = e.timedurationuntil - e.timestart;
|
|
} else if (e.duration == 2) {
|
|
e.timeduration = e.timedurationminutes * CoreConstants.SECONDS_MINUTE;
|
|
} else {
|
|
e.timeduration = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get options (name & value) for each allowed event type.
|
|
*
|
|
* @param eventTypes Result of getAllowedEventTypes.
|
|
* @return Options.
|
|
*/
|
|
getEventTypeOptions(eventTypes: any): {name: string, value: string}[] {
|
|
const options = [];
|
|
|
|
if (eventTypes.user) {
|
|
options.push({name: 'core.user', value: AddonCalendarProvider.TYPE_USER});
|
|
}
|
|
if (eventTypes.group) {
|
|
options.push({name: 'core.group', value: AddonCalendarProvider.TYPE_GROUP});
|
|
}
|
|
if (eventTypes.course) {
|
|
options.push({name: 'core.course', value: AddonCalendarProvider.TYPE_COURSE});
|
|
}
|
|
if (eventTypes.category) {
|
|
options.push({name: 'core.category', value: AddonCalendarProvider.TYPE_CATEGORY});
|
|
}
|
|
if (eventTypes.site) {
|
|
options.push({name: 'core.site', value: AddonCalendarProvider.TYPE_SITE});
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Get the month "id" (year + month).
|
|
*
|
|
* @param year Year.
|
|
* @param month Month.
|
|
* @return The "id".
|
|
*/
|
|
getMonthId(year: number, month: number): string {
|
|
return year + '#' + month;
|
|
}
|
|
|
|
/**
|
|
* Get weeks of a month in offline (with no events).
|
|
*
|
|
* The result has the same structure than getMonthlyEvents, but it only contains fields that are actually used by the app.
|
|
*
|
|
* @param year Year to get.
|
|
* @param month Month to get.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the response.
|
|
*/
|
|
getOfflineMonthWeeks(year: number, month: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
// Get starting week day user preference, fallback to site configuration.
|
|
const startWeekDay = site.getStoredConfig('calendar_startwday');
|
|
|
|
return this.configProvider.get(AddonCalendarProvider.STARTING_WEEK_DAY, startWeekDay);
|
|
}).then((startWeekDay) => {
|
|
const today = moment();
|
|
const isCurrentMonth = today.year() == year && today.month() == month - 1;
|
|
const weeks = [];
|
|
|
|
let date = moment({year, month: month - 1, date: 1});
|
|
for (let mday = 1; mday <= date.daysInMonth(); mday++) {
|
|
date = moment({year, month: month - 1, date: mday});
|
|
|
|
// Add new week and calculate prepadding.
|
|
if (!weeks.length || date.day() == startWeekDay) {
|
|
const prepaddingLength = (date.day() - startWeekDay + 7) % 7;
|
|
const prepadding = [];
|
|
for (let i = 0; i < prepaddingLength; i++) {
|
|
prepadding.push(i);
|
|
}
|
|
weeks.push({ prepadding, postpadding: [], days: []});
|
|
}
|
|
|
|
// Calculate postpadding of last week.
|
|
if (mday == date.daysInMonth()) {
|
|
const postpaddingLength = (startWeekDay - date.day() + 6) % 7;
|
|
const postpadding = [];
|
|
for (let i = 0; i < postpaddingLength; i++) {
|
|
postpadding.push(i);
|
|
}
|
|
weeks[weeks.length - 1].postpadding = postpadding;
|
|
}
|
|
|
|
// Add day to current week.
|
|
weeks[weeks.length - 1].days.push({
|
|
events: [],
|
|
hasevents: false,
|
|
mday: date.date(),
|
|
isweekend: date.day() == 0 || date.day() == 6,
|
|
istoday: isCurrentMonth && today.date() == date.date(),
|
|
calendareventtypes: [],
|
|
});
|
|
}
|
|
|
|
return {weeks, daynames: [{dayno: startWeekDay}]};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the data of an event has changed.
|
|
*
|
|
* @param data Current data.
|
|
* @param original Original data.
|
|
* @return True if data has changed, false otherwise.
|
|
*/
|
|
hasEventDataChanged(data: any, original?: any): boolean {
|
|
if (!original) {
|
|
// There is no original data, assume it hasn't changed.
|
|
return false;
|
|
}
|
|
|
|
// Check the fields that don't depend on any other.
|
|
if (data.name != original.name || data.timestart != original.timestart || data.eventtype != original.eventtype ||
|
|
data.description != original.description || data.location != original.location ||
|
|
data.duration != original.duration || data.repeat != original.repeat) {
|
|
return true;
|
|
}
|
|
|
|
// Check data that depends on eventtype.
|
|
if ((data.eventtype == AddonCalendarProvider.TYPE_CATEGORY && data.categoryid != original.categoryid) ||
|
|
(data.eventtype == AddonCalendarProvider.TYPE_COURSE && data.courseid != original.courseid) ||
|
|
(data.eventtype == AddonCalendarProvider.TYPE_GROUP && data.groupcourseid != original.groupcourseid &&
|
|
data.groupid != original.groupid)) {
|
|
return true;
|
|
}
|
|
|
|
// Check data that depends on duration.
|
|
if ((data.duration == 1 && data.timedurationuntil != original.timedurationuntil) ||
|
|
(data.duration == 2 && data.timedurationminutes != original.timedurationminutes)) {
|
|
return true;
|
|
}
|
|
|
|
if (data.repeat && data.repeats != original.repeats) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if an event should be displayed based on the filter.
|
|
*
|
|
* @param event Event object.
|
|
* @param courseId Course ID to filter.
|
|
* @param categoryId Category ID the course belongs to.
|
|
* @param categories Categories indexed by ID.
|
|
* @return Whether it should be displayed.
|
|
*/
|
|
shouldDisplayEvent(event: any, courseId: number, categoryId: number, categories: any): boolean {
|
|
if (event.eventtype == 'user' || event.eventtype == 'site') {
|
|
// User or site event, display it.
|
|
return true;
|
|
}
|
|
|
|
if (event.eventtype == 'category') {
|
|
if (!event.categoryid || !Object.keys(categories).length) {
|
|
// We can't tell if the course belongs to the category, display them all.
|
|
return true;
|
|
}
|
|
|
|
if (event.categoryid == categoryId) {
|
|
// The event is in the same category as the course, display it.
|
|
return true;
|
|
}
|
|
|
|
// Check parent categories.
|
|
let category = categories[categoryId];
|
|
while (category) {
|
|
if (!category.parent) {
|
|
// Category doesn't have parent, stop.
|
|
break;
|
|
}
|
|
|
|
if (event.categoryid == category.parent) {
|
|
return true;
|
|
}
|
|
category = categories[category.parent];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Show the event if it is from site home or if it matches the selected course.
|
|
return event.course && (event.course.id == this.sitesProvider.getCurrentSiteHomeId() || event.course.id == courseId);
|
|
}
|
|
|
|
/**
|
|
* Refresh the month & day for several created/edited/deleted events, and invalidate the months & days
|
|
* for their repeated events if needed.
|
|
*
|
|
* @param events Events that have been touched and number of times each event is repeated.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Resolved when done.
|
|
*/
|
|
refreshAfterChangeEvents(events: {event: any, repeated: number}[], siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const fetchTimestarts = [],
|
|
invalidateTimestarts = [];
|
|
|
|
// Always fetch upcoming events.
|
|
const upcomingPromise = this.calendarProvider.getUpcomingEvents(undefined, undefined, true, site.id).catch(() => {
|
|
// Ignore errors.
|
|
});
|
|
|
|
// Invalidate the events and get the timestarts so we can invalidate months & days.
|
|
return this.utils.allPromises([upcomingPromise].concat(events.map((eventData) => {
|
|
|
|
if (eventData.repeated > 1) {
|
|
if (eventData.event.repeatid) {
|
|
// Being edited or deleted.
|
|
// We need to calculate the days to invalidate because the event date could have changed.
|
|
// We don't know if the repeated events are before or after this one, invalidate them all.
|
|
fetchTimestarts.push(eventData.event.timestart);
|
|
|
|
for (let i = 1; i < eventData.repeated; i++) {
|
|
invalidateTimestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i);
|
|
invalidateTimestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i);
|
|
}
|
|
|
|
// Get the repeated events to invalidate them.
|
|
return this.calendarProvider.getLocalEventsByRepeatIdFromLocalDb(eventData.event.repeatid, site.id)
|
|
.then((events) => {
|
|
|
|
return this.utils.allPromises(events.map((event) => {
|
|
return this.calendarProvider.invalidateEvent(event.id);
|
|
}));
|
|
});
|
|
} else {
|
|
// Being added.
|
|
let time = eventData.event.timestart;
|
|
fetchTimestarts.push(time);
|
|
|
|
while (eventData.repeated > 1) {
|
|
time += CoreConstants.SECONDS_DAY * 7;
|
|
eventData.repeated--;
|
|
invalidateTimestarts.push(time);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
} else {
|
|
// Not repeated.
|
|
fetchTimestarts.push(eventData.event.timestart);
|
|
|
|
return this.calendarProvider.invalidateEvent(eventData.event.id);
|
|
}
|
|
|
|
}))).finally(() => {
|
|
const treatedMonths = {},
|
|
treatedDays = {};
|
|
|
|
return this.utils.allPromises([
|
|
this.calendarProvider.invalidateAllUpcomingEvents(),
|
|
|
|
// Fetch or invalidate months and days.
|
|
this.utils.allPromises(fetchTimestarts.concat(invalidateTimestarts).map((time, index) => {
|
|
const promises = [],
|
|
day = moment(new Date(time * 1000)),
|
|
monthId = this.getMonthId(day.year(), day.month() + 1),
|
|
dayId = monthId + '#' + day.date();
|
|
|
|
if (!treatedMonths[monthId]) {
|
|
// Month not treated already, do it now.
|
|
treatedMonths[monthId] = monthId;
|
|
|
|
if (index < fetchTimestarts.length) {
|
|
promises.push(this.calendarProvider.getMonthlyEvents(day.year(), day.month() + 1, undefined,
|
|
undefined, true, site.id).catch(() => {
|
|
|
|
// Ignore errors.
|
|
}));
|
|
} else {
|
|
promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id));
|
|
}
|
|
}
|
|
|
|
if (!treatedDays[dayId]) {
|
|
// Day not invalidated already, do it now.
|
|
treatedDays[dayId] = dayId;
|
|
|
|
if (index < fetchTimestarts.length) {
|
|
promises.push(this.calendarProvider.getDayEvents(day.year(), day.month() + 1, day.date(),
|
|
undefined, undefined, true, site.id).catch(() => {
|
|
|
|
// Ignore errors.
|
|
}));
|
|
} else {
|
|
promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(),
|
|
site.id));
|
|
}
|
|
}
|
|
|
|
return this.utils.allPromises(promises);
|
|
}))
|
|
]);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refresh the month & day for a created/edited/deleted event, and invalidate the months & days
|
|
* for their repeated events if needed.
|
|
*
|
|
* @param event Event that has been touched.
|
|
* @param repeated Number of times the event is repeated.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Resolved when done.
|
|
*/
|
|
refreshAfterChangeEvent(event: any, repeated: number, siteId?: string): Promise<any> {
|
|
return this.refreshAfterChangeEvents([{event: event, repeated: repeated}], siteId);
|
|
}
|
|
}
|