From 752a97a359d8852ee25012f3eacdaeb7b8eaed37 Mon Sep 17 00:00:00 2001 From: Tinashe <70011086+tinashechiraya@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:53:34 +0200 Subject: [PATCH] fix invitations not sending (#242) * fix invitations not sending * fix invitations not sending --------- Co-authored-by: tinashe --- django_project/base/organisation_views.py | 99 +++++++++++-------- .../invitation_to_join_organization.html | 10 +- .../frontend/src/components/inviteMembers.tsx | 61 ++++++++++-- .../pages/OrganisationInformation/index.tsx | 2 +- .../frontend/src/store/organizationSlice.ts | 30 ++++-- 5 files changed, 140 insertions(+), 62 deletions(-) diff --git a/django_project/base/organisation_views.py b/django_project/base/organisation_views.py index de94d2ef..23cf5ab0 100644 --- a/django_project/base/organisation_views.py +++ b/django_project/base/organisation_views.py @@ -325,20 +325,23 @@ def fetch_organisation_data(request): .values("user_profile__user__email", "user_type") ) - + # Fetch invitations from OrganisationInvitationDetail + # where request_type is 'join_organisation' invitations = list( - org.custom_invitations.all().values("email", "accepted") - ) if org.custom_invitations.exists() else [] + OrganisationInvitationDetail.objects.filter( + organisation=org, + request_type="join_organisation" + ).values("invitation__email", "accepted") + ) # Check if the user is a manager - is_manager = user_org.user_type == 'manager' - + is_manager = user_org.user_type == "manager" data[org.name] = { "org_id": org.id, "members": members, "invitations": invitations, - "is_manager": is_manager + "is_manager": is_manager, } return JsonResponse(data, status=200) @@ -347,24 +350,24 @@ def fetch_organisation_data(request): return JsonResponse( { "error": "User profile is not set up correctly.", - "details": str(e) + "details": str(e), }, - status=500 + status=500, ) except Exception as e: return JsonResponse( {"error": "An unexpected error occurred.", "details": str(e)}, - status=500 + status=500, ) + @login_required def invite_to_organisation(request, organisation_id): """View to invite a user to an organisation.""" try: - # Ensure the organisation exists organisation = get_object_or_404(Organisation, id=organisation_id) if request.method != 'POST': @@ -373,7 +376,6 @@ def invite_to_organisation(request, organisation_id): status=405 ) - # Parse JSON payload try: data = json.loads(request.body) except json.JSONDecodeError: @@ -389,7 +391,6 @@ def invite_to_organisation(request, organisation_id): email = form.cleaned_data["email"] message = data.get("message", "") - # Check if the user has permission to invite (using UserOrganisations) user_profile = get_object_or_404(UserProfile, user=request.user) user_organisation = UserOrganisations.objects.filter( user_profile=user_profile, @@ -402,41 +403,50 @@ def invite_to_organisation(request, organisation_id): status=403 ) - # Check if an invitation already exists - invitation = OrganisationInvitation.objects.filter( + invitation, created = OrganisationInvitation.objects.get_or_create( email=email, - organisation=organisation - ).first() - if not invitation: - invitation = OrganisationInvitation.objects.create( - email=email, - inviter=request.user, - organisation=organisation + organisation=organisation, + defaults={'inviter': request.user} + ) + + # Check if an invitation detail already exists + invitation_detail, detail_created = ( + OrganisationInvitationDetail.objects.get_or_create( + invitation=invitation, + organisation=organisation, + defaults={'request_type': 'join_organisation'} ) + ) - # Send the invitation - try: - invitation.send_invitation( - request=request, - custom_message=message - ) - return JsonResponse({"success": True}, status=200) - except Exception as e: - return JsonResponse( - { - "error": "Failed to send invitation.", - "details": str(e) - }, status=500) + if not created or not detail_created: + return JsonResponse( + { + "error": + "Invitation already sent to member to join organisation." + }, + status=400 + ) + # Send the invitation if newly created + try: + invitation.send_invitation( + request=request, + custom_message=message + ) + return JsonResponse({"success": True}, status=200) + except Exception as e: + return JsonResponse( + {"error": "Failed to send invitation.", "details": str(e)}, + status=500 + ) except Organisation.DoesNotExist: return JsonResponse({"error": "Organisation not found."}, status=404) except Exception as e: - print(str(e)) return JsonResponse( - { - "error": "An unexpected error occurred", - "details": str(e) - }, status=500) + {"error": "An unexpected error occurred", "details": str(e)}, + status=500 + ) + @@ -469,9 +479,7 @@ def accept_invite(request, invitation_id): if user_org: return JsonResponse( - { - "error": "You are already a member of this organization." - }, + {"error": "You are already a member of this organization."}, status=400 ) @@ -482,8 +490,13 @@ def accept_invite(request, invitation_id): user_type='member' ) - user_profile.organisations.add(invitation.organisation) user_profile.save() + # Update invitation detail as accepted + OrganisationInvitationDetail.objects.filter( + invitation=invitation, + organisation=invitation.organisation + ).update(accepted=True) + return redirect(f"{reverse('home')}?invitation_accepted=true") diff --git a/django_project/base/templates/invitation_to_join_organization.html b/django_project/base/templates/invitation_to_join_organization.html index e864fb22..b39c4eb1 100644 --- a/django_project/base/templates/invitation_to_join_organization.html +++ b/django_project/base/templates/invitation_to_join_organization.html @@ -22,22 +22,22 @@

Africa Rangeland Watch

{paginatedInvitations.map((invite: any, idx: number) => ( - +
-

[Organisation Manager] has invited you to join their [Organisation]

+

Organisation Manager has invited you to join {{ organisation }}

- [Organisation Manager] has invited you to join their [Organisation] on the Africa Rangeland Watch platform. We’re excited to have you onboard and look forward to supporting you as part of the organisation. + Organisation Manager has invited you to join their {{ organisation }} on the Africa Rangeland Watch platform. We’re excited to have you onboard and look forward to supporting you as part of the organisation.

- To get started, simply click the link below to create your account and officially join the [Organisation]: + To get started, simply click the link below to create your account and officially join the {{ organisation }}:

- Join [Organisation] + Join {{ organisation }}

- Once you complete your signup, you’ll have access to all of [Organisation] resources on the Africa Rangeland Watch platform. + Once you complete your signup, you’ll have access to all of {{ organisation }} resources on the Africa Rangeland Watch platform.

If you have any questions along the way, please reach out to our support team at [Support Email]. diff --git a/django_project/frontend/src/components/inviteMembers.tsx b/django_project/frontend/src/components/inviteMembers.tsx index 2aae92c5..1760797d 100644 --- a/django_project/frontend/src/components/inviteMembers.tsx +++ b/django_project/frontend/src/components/inviteMembers.tsx @@ -13,11 +13,12 @@ import { ModalBody, useBreakpointValue, Textarea, + useToast, } from "@chakra-ui/react"; - import React, { useState } from "react"; - import { useDispatch } from "react-redux"; - import { inviteMember } from "../store/organizationSlice"; -import { AppDispatch } from "../store"; + import React, { useEffect, useState } from "react"; + import { useDispatch, useSelector } from "react-redux"; + import { updateState, inviteMemberThunk, clearResponseMessage } from "../store/organizationSlice"; +import { AppDispatch, RootState } from "../store"; interface Props { isOpen: boolean; @@ -29,6 +30,9 @@ import { AppDispatch } from "../store"; type ModalPosition = "absolute" | "fixed"; export default function InviteMember({ isOpen, onClose, orgKey, organizationName }: Props) { + const { error, loading ,invite_sent } = useSelector((state: RootState) => state.organization); + const toast = useToast(); + const modalPosition = useBreakpointValue({ base: "absolute", md: "fixed", @@ -45,12 +49,57 @@ import { AppDispatch } from "../store"; const dispatch = useDispatch(); - const handleInvite = () => { + const handleInvite = async () => { if (email && message) { - dispatch(inviteMember({ orgKey, email, message })); + const parsedOrgKey = parseInt(orgKey, 10); + const result = await dispatch( + inviteMemberThunk({ orgKey: parsedOrgKey, email, message }) + ).unwrap(); + dispatch(updateState({ orgKey: String(result.orgKey), email: result.email, message })); onClose(); } }; + + + useEffect(() => { + + if(!loading){ + + if(error){ + toast({ + title: "Request failed. Please try again later", + description: error, + status: "error", + duration: 5000, + isClosable: true, + position: "top-right", + containerStyle: { + backgroundColor: "red", + color: "white", + }, + }); + }else { + if(invite_sent) + toast({ + title: "Invitation sent.", + description: "Your request has been submitted successfully.", + status: "success", + duration: 5000, + isClosable: true, + position: "top-right", + containerStyle: { + backgroundColor: "#00634b", + color: "white", + }, + }) + } + dispatch(clearResponseMessage()); + + + } + + }, [error, loading]); + return ( diff --git a/django_project/frontend/src/pages/OrganisationInformation/index.tsx b/django_project/frontend/src/pages/OrganisationInformation/index.tsx index c261b59d..481b63ee 100644 --- a/django_project/frontend/src/pages/OrganisationInformation/index.tsx +++ b/django_project/frontend/src/pages/OrganisationInformation/index.tsx @@ -297,7 +297,7 @@ export default function OrganisationInformation() {

{invite.email}{invite.invitation__email} { setCSRFToken(); - const response = await axios.post(`/api/organization/${orgKey}/invite/`, { - email, - message, - }); - return { orgKey, email }; + try { + const response = await axios.post(`/api/organization/${orgKey}/invite/`, { + email, + message, + }); + return { orgKey, email }; // Return success response + } catch (error: any) { + // If the error is caught, throw a custom error + throw new Error(error.response?.data?.error || "Failed to send invitation."); + } } ); + // Async thunk for deleting a member from an organization export const deleteMember = createAsyncThunk( "organizations/deleteMember", @@ -81,6 +89,10 @@ const organizationsSlice = createSlice({ name: "organizations", initialState, reducers: { + clearResponseMessage: (state) => { + state.error = null; + state.invite_sent = false; + }, addMember: (state, action: PayloadAction<{ orgKey: string; member: Member }>) => { const { orgKey, member } = action.payload; state.organizations[orgKey]?.members.push(member); @@ -91,7 +103,7 @@ const organizationsSlice = createSlice({ (member) => member.user !== user ); }, - inviteMember: (state, action: PayloadAction<{ orgKey: string; email: string; message: string }>) => { + updateState: (state, action: PayloadAction<{ orgKey: string; email: string; message: string }>) => { const { orgKey, email, message } = action.payload; const newInvitation: Invitation = { email, @@ -125,6 +137,7 @@ const organizationsSlice = createSlice({ .addCase(inviteMemberThunk.pending, (state) => { state.loading = true; state.error = null; + state.invite_sent = false; }) .addCase(inviteMemberThunk.fulfilled, (state, action) => { const { orgKey, email } = action.payload; @@ -135,10 +148,12 @@ const organizationsSlice = createSlice({ state.organizations[orgKey]?.invitations.push(invitation); state.loading = false; state.refetch = true; + state.invite_sent = true; }) .addCase(inviteMemberThunk.rejected, (state, action) => { state.error = action.error.message || "Failed to send invitation."; state.loading = false; + state.invite_sent = false }) .addCase(deleteMember.pending, (state) => { state.loading = true; @@ -157,5 +172,6 @@ const organizationsSlice = createSlice({ }); export const selectRefetch = (state: RootState) => state.organization.refetch; -export const { addMember, deleteMemberFromState, inviteMember, resetRefetch } = organizationsSlice.actions; +export const { addMember, deleteMemberFromState, updateState, resetRefetch, clearResponseMessage } = organizationsSlice.actions; export default organizationsSlice.reducer; + \ No newline at end of file