Skip to content

Commit

Permalink
feat: Recurring events improvements and additions (PalisadoesFoundati…
Browse files Browse the repository at this point in the history
…on#2234)

* add fields and field resolvers for recurrenceRule

* add tests for field resolvers

* minor corrections

* improve recurring events implementation

* modify the code for updating recurring events as per the new implementation

* fix tests for deleting recurring events

* add more checks and tests

* remove dangling recurrence rule and base recurring event documents

* remove adminRemoveEvent mutation

* minor correction

* minor corrections

* make the instance an exception on thisInstance updates

* create a new series on event instance duration change

* minor correction
  • Loading branch information
meetulr authored Apr 19, 2024
1 parent a75e88b commit 24ef15d
Show file tree
Hide file tree
Showing 51 changed files with 2,017 additions and 1,191 deletions.
2 changes: 2 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"actionItem.notFound": "Action Item not found",
"advertisement.notFound": "Advertisement not found",
"event.notFound": "Event not found",
"baseRecurringEvent.notFound": "Base Recurring Event not found",
"recurrenceRule.notFound": "Recurrence Rule not found",
"organization.notFound": "Organization not found",
"organization.profileImage.notFound": "Organization profile image not found",
"organization.member.notFound": "Organization's user is not a member",
Expand Down
2 changes: 2 additions & 0 deletions locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"actionItemCategory.isDisabled": "La catégorie d'élément d'action est désactivée",
"actionItem.notFound": "Élément d\\’action non trouvé",
"event.notFound": "Événement non trouvé",
"baseRecurringEvent.notFound": "Événement récurrent de base introuvable",
"recurrenceRule.notFound": "Règle de récurrence introuvable",
"organization.notFound": "Organisation introuvable",
"organization.profileImage.notFound": "Image du profil de l'organisation introuvable",
"organization.member.notFound": "L'utilisateur de l'organisation n'est pas membre",
Expand Down
2 changes: 2 additions & 0 deletions locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"actionItem.notFound": "कार्रवाई का मद नहीं मिला",
"advertisement.notFound": "विज्ञापन नहीं मिला",
"event.notFound": "घटना नहीं मिली",
"baseRecurringEvent.notFound": "आधार पुनरावृत्ति कार्यक्रम नहीं मिला",
"recurrenceRule.notFound": "पुनरावृत्ति नियम नहीं मिला",
"organization.notFound": "संगठन नहीं मिला",
"organization.profileImage.notFound": "संगठन की प्रोफ़ाइल छवि नहीं मिली",
"organization.member.notFound": "संगठन का उपयोगकर्ता सदस्य नहीं है",
Expand Down
2 changes: 2 additions & 0 deletions locales/sp.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"actionItemCategory.isDisabled": "La categoría de elemento de acción está deshabilitada",
"actionItem.notFound": "Elemento de acción no encontrado",
"event.notFound": "Evento no encontrado",
"baseRecurringEvent.notFound": "Evento recurrente base no encontrado",
"recurrenceRule.notFound": "Regla de recurrencia no encontrada",
"organization.notFound": "Organización no encontrada",
"organization.profileImage.notFound": "No se encontró la imagen del perfil de la organización",
"organization.member.notFound": "El usuario de la organización no es miembro",
Expand Down
2 changes: 2 additions & 0 deletions locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"actionItemCategory.isDisabled": "操作项类别已禁用",
"actionItem.notFound": "找不到操作项",
"event.notFound": "未找到事件",
"baseRecurringEvent.notFound": "未找到基本重复事件",
"recurrenceRule.notFound": "未找到重复规则",
"organization.notFound": "未找到組織",
"organization.profileImage.notFound": "未找到組織檔案圖像",
"organization.member.notFound": "組織的用戶不是成員",
Expand Down
27 changes: 17 additions & 10 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ type Event {
attendees: [User]
attendeesCheckInStatus: [CheckInStatus!]!
averageFeedbackScore: Float
baseRecurringEvent: Event
createdAt: DateTime!
creator: User
description: String!
Expand All @@ -628,6 +629,7 @@ type Event {
feedback: [Feedback!]!
images: [String]
isPublic: Boolean!
isRecurringEventException: Boolean!
isRegisterable: Boolean!
latitude: Latitude
location: String
Expand Down Expand Up @@ -664,7 +666,7 @@ input EventAttendeeInput {
input EventInput {
allDay: Boolean!
description: String!
endDate: Date
endDate: Date!
endTime: Time
images: [String]
isPublic: Boolean!
Expand Down Expand Up @@ -1032,7 +1034,6 @@ type MinimumValueError implements FieldError {
}

type Mutation {
acceptAdmin(id: ID!): Boolean!
acceptMembershipRequest(membershipRequestId: ID!): MembershipRequest!
addEventAttendee(data: EventAttendeeInput!): User!
addFeedback(data: FeedbackInput!): Feedback!
Expand All @@ -1044,7 +1045,6 @@ type Mutation {
addUserImage(file: String!): User!
addUserToGroupChat(chatId: ID!, userId: ID!): GroupChat!
addUserToUserFamily(familyId: ID!, userId: ID!): UserFamily!
adminRemoveEvent(eventId: ID!): Event!
adminRemoveGroup(groupId: ID!): GroupChat!
assignUserTag(input: ToggleUserTagAssignInput!): User
blockPluginCreationBySuperadmin(blockUser: Boolean!, userId: ID!): AppUserProfile!
Expand Down Expand Up @@ -1096,7 +1096,6 @@ type Mutation {
refreshToken(refreshToken: String!): ExtendSession!
registerEventAttendee(data: EventAttendeeInput!): EventAttendee!
registerForEvent(id: ID!): EventAttendee!
rejectAdmin(id: ID!): Boolean!
rejectMembershipRequest(membershipRequestId: ID!): MembershipRequest!
removeActionItem(id: ID!): ActionItem!
removeAdmin(data: UserAndOrganizationInput!): AppUserProfile!
Expand Down Expand Up @@ -1145,7 +1144,7 @@ type Mutation {
updateAgendaItem(id: ID!, input: UpdateAgendaItemInput!): AgendaItem
updateAgendaSection(id: ID!, input: UpdateAgendaSectionInput!): AgendaSection
updateCommunity(data: UpdateCommunityInput!): Boolean!
updateEvent(data: UpdateEventInput, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventMutationType): Event!
updateEvent(data: UpdateEventInput!, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventMutationType): Event!
updateEventVolunteer(data: UpdateEventVolunteerInput, id: ID!): EventVolunteer!
updateEventVolunteerGroup(data: UpdateEventVolunteerGroupInput, id: ID!): EventVolunteerGroup!
updateFund(data: UpdateFundInput!, id: ID!): Fund!
Expand Down Expand Up @@ -1472,9 +1471,15 @@ input RecaptchaVerification {
}

type RecurrenceRule {
baseRecurringEvent: Event
count: PositiveInt
frequency: Frequency
interval: PositiveInt
frequency: Frequency!
interval: PositiveInt!
latestInstanceDate: Date
organization: Organization
recurrenceEndDate: Date
recurrenceRuleString: String!
recurrenceStartDate: Date!
weekDayOccurenceInMonth: Int
weekDays: [WeekDays]
}
Expand All @@ -1483,14 +1488,16 @@ input RecurrenceRuleInput {
count: PositiveInt
frequency: Frequency
interval: PositiveInt
recurrenceEndDate: Date
recurrenceStartDate: Date
weekDayOccurenceInMonth: Int
weekDays: [WeekDays]
}

enum RecurringEventMutationType {
AllInstances
ThisAndFollowingInstances
ThisInstance
allInstances
thisAndFollowingInstances
thisInstance
}

type SocialMediaUrls {
Expand Down
16 changes: 16 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ACTION_ITEM_CATEGORY_ALREADY_EXISTS = {
MESSAGE: "actionItemCategory.alreadyExists",
PARAM: "actionItemCategory",
};

export const ACTION_ITEM_CATEGORY_IS_DISABLED = {
DESC: "Action Item Category is disabled",
CODE: "actionItemCategory.isDisabled",
Expand All @@ -42,6 +43,13 @@ export const AGENDA_CATEGORY_NOT_FOUND_ERROR = {
PARAM: "agendaCategory",
};

export const BASE_RECURRING_EVENT_NOT_FOUND = {
DESC: "Base Recurring Event not found",
CODE: "baseRecurringEvent.notFound",
MESSAGE: "baseRecurringEvent.notFound",
PARAM: "baseRecurringEvent",
};

export const CHAT_NOT_FOUND_ERROR = {
DESC: "Chat not found",
CODE: "chat.notFound",
Expand Down Expand Up @@ -174,6 +182,14 @@ export const ORGANIZATION_NOT_FOUND_ERROR = {
MESSAGE: "organization.notFound",
PARAM: "organization",
};

export const RECURRENCE_RULE_NOT_FOUND = {
DESC: "Recurrence Rule not found",
CODE: "recurrenceRule.notFound",
MESSAGE: "recurrenceRule.notFound",
PARAM: "recurrenceRule",
};

export const VENUE_NAME_MISSING_ERROR = {
DESC: "Venue name not found",
CODE: "venueName.notFound",
Expand Down
22 changes: 12 additions & 10 deletions src/helpers/event/createEventHelpers/createRecurringEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,19 @@ export const createRecurringEvent = async (
let { recurrenceRuleData } = args;

if (!recurrenceRuleData) {
// create a default weekly recurrence rule
// create a default recurrence rule -> infinite weekly recurrence
recurrenceRuleData = {
frequency: "WEEKLY",
recurrenceStartDate: data.startDate,
recurrenceEndDate: null,
};
}

const { recurrenceStartDate, recurrenceEndDate } = recurrenceRuleData;

// generate a recurrence rule string which would be used to generate rrule object
// and get recurrence dates
const recurrenceRuleString = generateRecurrenceRuleString(
recurrenceRuleData,
data?.startDate,
data?.endDate,
);
const recurrenceRuleString = generateRecurrenceRuleString(recurrenceRuleData);

// create a base recurring event first, based on which all the
// recurring instances would be dynamically generated
Expand All @@ -55,6 +55,8 @@ export const createRecurringEvent = async (
{
...data,
recurring: true,
startDate: recurrenceStartDate,
endDate: recurrenceEndDate,
isBaseRecurringEvent: true,
creatorId,
admins: [creatorId],
Expand All @@ -68,8 +70,8 @@ export const createRecurringEvent = async (
// to be generated in this operation (rest would be generated dynamically during query)
const recurringInstanceDates = getRecurringInstanceDates(
recurrenceRuleString,
data.startDate,
data.endDate,
recurrenceStartDate,
recurrenceEndDate,
);

// get the date for the latest created instance
Expand All @@ -78,8 +80,8 @@ export const createRecurringEvent = async (
// create a recurrenceRule document that would contain the recurrence pattern
const recurrenceRule = await createRecurrenceRule(
recurrenceRuleString,
data.startDate,
data.endDate,
recurrenceStartDate,
recurrenceEndDate,
organizationId,
baseRecurringEvent[0]?._id.toString(),
latestInstanceDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { addDays, addYears } from "date-fns";
import { RecurrenceRule } from "../../../models/RecurrenceRule";
import { convertToUTCDate } from "../../../utilities/recurrenceDatesUtil";
import { Event } from "../../../models";
import { Event, RecurrenceRule } from "../../../models";
import {
generateRecurringEventInstances,
getRecurringInstanceDates,
Expand Down Expand Up @@ -62,9 +61,9 @@ export const createRecurringEventInstancesDuringQuery = async (
// get the properties from recurrenceRule
const {
_id: recurrenceRuleId,
latestInstanceDate,
recurrenceEndDate,
recurrenceRuleString,
endDate: recurrenceEndDate,
latestInstanceDate,
count: totalInstancesCount,
} = recurrenceRule;

Expand Down
49 changes: 38 additions & 11 deletions src/helpers/event/deleteEventHelpers/deleteRecurringEvent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type mongoose from "mongoose";
import type { InterfaceEvent } from "../../../models";
import { Event } from "../../../models";
import { RecurrenceRule } from "../../../models/RecurrenceRule";
import { Event, RecurrenceRule } from "../../../models";
import type { MutationRemoveEventArgs } from "../../../types/generatedGraphQLTypes";
import { deleteRecurringEventInstances, deleteSingleEvent } from "./index";
import { errors, requestContext } from "../../../libraries";
import {
BASE_RECURRING_EVENT_NOT_FOUND,
RECURRENCE_RULE_NOT_FOUND,
} from "../../../constants";

/**
* This function deletes thisInstance / allInstances / thisAndFollowingInstances of a recurring event.
Expand All @@ -21,38 +25,61 @@ export const deleteRecurringEvent = async (
session: mongoose.ClientSession,
): Promise<void> => {
// get the recurrenceRule
const recurrenceRule = await RecurrenceRule.find({
const recurrenceRule = await RecurrenceRule.findOne({
_id: event.recurrenceRuleId,
});

// throws error if the recurrence rule doesn't exist
if (recurrenceRule === null) {
throw new errors.NotFoundError(
requestContext.translate(RECURRENCE_RULE_NOT_FOUND.MESSAGE),
RECURRENCE_RULE_NOT_FOUND.CODE,
RECURRENCE_RULE_NOT_FOUND.PARAM,
);
}

// get the baseRecurringEvent
const baseRecurringEvent = await Event.find({
const baseRecurringEvent = await Event.findOne({
_id: event.baseRecurringEventId,
});

// throws error if the base recurring event doesn't exist
if (baseRecurringEvent === null) {
throw new errors.NotFoundError(
requestContext.translate(BASE_RECURRING_EVENT_NOT_FOUND.MESSAGE),
BASE_RECURRING_EVENT_NOT_FOUND.CODE,
BASE_RECURRING_EVENT_NOT_FOUND.PARAM,
);
}

if (
event.isRecurringEventException ||
args.recurringEventDeleteType === "ThisInstance"
args.recurringEventDeleteType === "thisInstance"
) {
// if the event is an exception or if it's deleting thisInstance only,
// just delete this single instance
await deleteSingleEvent(event._id.toString(), session);
} else if (args.recurringEventDeleteType === "AllInstances") {
await deleteSingleEvent(
event._id.toString(),
session,
recurrenceRule._id.toString(),
baseRecurringEvent._id.toString(),
);
} else if (args.recurringEventDeleteType === "allInstances") {
// delete all the instances
// and update the recurrenceRule and baseRecurringEvent accordingly
await deleteRecurringEventInstances(
null, // because we're going to delete all the instances, which we could get from the recurrence rule
recurrenceRule[0],
baseRecurringEvent[0],
recurrenceRule,
baseRecurringEvent,
session,
);
} else {
// delete this and following the instances
// and update the recurrenceRule and baseRecurringEvent accordingly
await deleteRecurringEventInstances(
event, // we'll find all the instances after(and including) this one and delete them
recurrenceRule[0],
baseRecurringEvent[0],
recurrenceRule,
baseRecurringEvent,
session,
);
}
Expand Down
Loading

0 comments on commit 24ef15d

Please sign in to comment.