Skip to content

Commit

Permalink
fix invitations not sending (#242)
Browse files Browse the repository at this point in the history
* fix invitations not sending

* fix invitations not sending

---------

Co-authored-by: tinashe <[email protected]>
  • Loading branch information
tinashechiraya and tinashe authored Jan 31, 2025
1 parent ddfc730 commit 752a97a
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 62 deletions.
99 changes: 56 additions & 43 deletions django_project/base/organisation_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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':
Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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
)




Expand Down Expand Up @@ -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
)

Expand All @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ <h2 style="margin: 0; font-size: 24px;">Africa Rangeland Watch</h2>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="900" style="background-color: #ffffff; padding: 20px; border: 10px solid #dddddd;">
<tr>
<td style="padding: 20px;">
<h2 style="font-size: 20px; color: #333333;"><a href="[Organisation Manager]" style="color: #FFA500;">[Organisation Manager]</a> has invited you to join their <a href="[Organisation]" style="color: #FFA500;">[Organisation]</a></h2>
<h2 style="font-size: 20px; color: #333333;"><a href="[Organisation Manager]" style="color: #FFA500;">Organisation Manager</a> has invited you to join <a href="{{organisation}}" style="color: #FFA500;">{{ organisation }}</a></h2>
<p style="font-size: 16px; color: #333333;">
<a href="mailto:[Organisation Manager]" style="color: #FFA500;">[Organisation Manager]</a> has invited you to join their <a href="mailto:[Organisation]" style="color: #FFA500;">[Organisation]</a> on the Africa Rangeland Watch platform. We’re excited to have you onboard and look forward to supporting you as part of the organisation.
<a href="mailto:[Organisation Manager]" style="color: #FFA500;">Organisation Manager</a> has invited you to join their <a href="mailto:[Organisation]" style="color: #FFA500;">{{ organisation }}</a> on the Africa Rangeland Watch platform. We’re excited to have you onboard and look forward to supporting you as part of the organisation.
</p>
<p style="font-size: 16px; color: #333333;">
To get started, simply click the link below to create your account and officially join the <a href="mailto:[Organisation]" style="color: #FFA500;">[Organisation]</a>:
To get started, simply click the link below to create your account and officially join the <a href="mailto:[Organisation]" style="color: #FFA500;">{{ organisation }}</a>:
</p>
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 20px 0;">
<a href="[Create Account Link]" style="background-color: #FFA500; color: #ffffff; padding: 15px 50px; text-decoration: none; font-size: 16px; border-radius: 5px;">Join [Organisation]</a>
<a href="{{accept_url}}" style="background-color: #FFA500; color: #ffffff; padding: 15px 50px; text-decoration: none; font-size: 16px; border-radius: 5px;">Join {{ organisation }}</a>
</td>
</tr>
</table>
<p style="font-size: 16px; color: #333333;">
Once you complete your signup, you’ll have access to all of <a href="[Organisation]" style="color: #FFA500;">[Organisation]</a> resources on the Africa Rangeland Watch platform.
Once you complete your signup, you’ll have access to all of <a href="[Organisation]" style="color: #FFA500;">{{ organisation }}</a> resources on the Africa Rangeland Watch platform.
</p>
<p style="font-size: 16px; color: #333333;">
If you have any questions along the way, please reach out to our support team at <a href="mailto:[Support Email]" style="color: #FFA500;">[Support Email]</a>.
Expand Down
61 changes: 55 additions & 6 deletions django_project/frontend/src/components/inviteMembers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ModalPosition>({
base: "absolute",
md: "fixed",
Expand All @@ -45,12 +49,57 @@ import { AppDispatch } from "../store";

const dispatch = useDispatch<AppDispatch>();

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 (
<Modal isOpen={isOpen} onClose={onClose} isCentered={modalPosition === "absolute"}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export default function OrganisationInformation() {
<Tbody>
{paginatedInvitations.map((invite: any, idx: number) => (
<Tr key={idx}>
<Td>{invite.email}</Td>
<Td>{invite.invitation__email}</Td>
<Td>
<Badge
backgroundColor={
Expand Down
30 changes: 23 additions & 7 deletions django_project/frontend/src/store/organizationSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface Organization {
interface OrganizationsState {
organizations: { [key: string]: Organization };
loading: boolean;
invite_sent: boolean;
error: string | null;
refetch: boolean;
}
Expand All @@ -30,6 +31,7 @@ interface OrganizationsState {
const initialState: OrganizationsState = {
organizations: {},
loading: false,
invite_sent: false,
error: null,
refetch: false,
};
Expand All @@ -53,14 +55,20 @@ export const inviteMemberThunk = createAsyncThunk(
"organizations/inviteMember",
async ({ orgKey, email, message }: { orgKey: number; email: string; message: string }) => {
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",
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

0 comments on commit 752a97a

Please sign in to comment.