Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix invitations not sending #242

Merged
merged 3 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;