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

feat(organizations): create endpoints to handle organization invitations #5395

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

rajpatel24
Copy link
Contributor

@rajpatel24 rajpatel24 commented Dec 20, 2024

🗒️ Checklist

  1. run linter locally
  2. update all related docs (API, README, inline, etc.), if any
  3. draft PR with a title <type>(<scope>)<!>: <title> TASK-1234
  4. tag PR: at least frontend or backend unless it's global
  5. fill in the template below and delete template comments
  6. review thyself: read the diff and repro the preview as written
  7. open PR & confirm that CI passes
  8. request reviewers, if needed
  9. delete this section before merging

📣 Summary

Implemented endpoints for organization invitations, allowing organization owners to invite existing users or unregistered users to join their organization. The invitee can either accept or decline the invitation. If the invitee accepts, their assets will be transferred to the organization.

📖 Description

  • Organization owners can send invitations to users (both registered and unregistered) via email or username.
  • The invitee can accept or decline the invitation. If accepted, the invitee's assets will be transferred to the organization owner.

POST https://[kpi]/api/v2/organizations/org_12345/invites/

  • Create organization invites for registered and unregistered users.
  • Set the role for which the user is being invited - (Choices: member, admin). Default is member.

Payload:

{
    "invitees": ["demo14", "[email protected]", "[email protected]"],
    "role": "admin"
}

Response:

[
    {
        "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/7a4f9a3b-9112-43cc-a6ae-bb4a6583b4b2/",
        "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
        "status": "pending",
        "invitee_role": "admin",
        "created": "2025-01-06T13:01:40Z",
        "modified": "2025-01-06T13:01:40Z",
        "invitee": "demo14"
    },
    {
        "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/6746121a-7a87-4c2d-9994-85e38d8cff65/",
        "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
        "status": "pending",
        "invitee_role": "admin",
        "created": "2025-01-06T13:01:40Z",
        "modified": "2025-01-06T13:01:40Z",
        "invitee": "demo13"
    },
    {
        "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/2af706a9-4f67-4145-a0d3-8d66fbc77a19/",
        "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
        "status": "pending",
        "invitee_role": "admin",
        "created": "2025-01-06T13:01:40Z",
        "modified": "2025-01-06T13:01:40Z",
        "invitee": "[email protected]"
    }
]

GET https://[kpi]/api/v2/organizations/org_12345/invites/

Response:

{
    "count": 3,
    "next": null,
    "previous": null,
    "results": [
        {
            "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/6746121a-7a87-4c2d-9994-85e38d8cff65/",
            "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
            "status": "pending",
            "invitee_role": "admin",
            "created": "2025-01-06T13:01:40Z",
            "modified": "2025-01-06T13:01:40Z",
            "invitee": "demo13"
        },
        {
            "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/7a4f9a3b-9112-43cc-a6ae-bb4a6583b4b2/",
            "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
            "status": "pending",
            "invitee_role": "admin",
            "created": "2025-01-06T13:01:40Z",
            "modified": "2025-01-06T13:01:40Z",
            "invitee": "demo14"
        },
        {
            "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/2af706a9-4f67-4145-a0d3-8d66fbc77a19/",
            "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
            "status": "pending",
            "invitee_role": "admin",
            "created": "2025-01-06T13:01:40Z",
            "modified": "2025-01-06T13:01:40Z",
            "invitee": "[email protected]"
        }
    ]
}

PATCH https://[kpi]/api/v2/organizations/org_12345/invites/7a4f9a3b-9112-43cc-a6ae-bb4a6583b4b2/

  • Update an organization invite to accept, decline, cancel, expire, or resend.

Payload:

{
    "status": "accepted"
}

Response:

{
    "url": "http://kf.kobo.local/api/v2/organizations/org_12345/invites/7a4f9a3b-9112-43cc-a6ae-bb4a6583b4b2/",
    "invited_by": "http://kf.kobo.local/api/v2/users/gtl_raj/",
    "status": "accepted",
    "invitee_role": "admin",
    "created": "2025-01-06T13:01:40Z",
    "modified": "2025-01-06T13:01:40Z",
    "invitee": "demo14"
}

DELETE https://[kpi]/api/v2/organizations/org_12345/invites/7a4f9a3b-9112-43cc-a6ae-bb4a6583b4b2/

  • Organization owner or admin can delete an organization invite.

Response: 204

@rajpatel24 rajpatel24 force-pushed the task-969-create-endpoints-to-handle-org-invitations branch from 092d932 to 64a2b55 Compare December 30, 2024 15:46
@rajpatel24 rajpatel24 changed the title Create endpoints to handle organization invitations feat(organizations): create endpoints to handle organization invitations Dec 30, 2024
@rajpatel24 rajpatel24 marked this pull request as ready for review December 30, 2024 16:14
@rajpatel24 rajpatel24 removed the request for review from jnm December 30, 2024 16:14
Copy link

Copy link

Copy link

@rajpatel24 rajpatel24 force-pushed the task-969-create-endpoints-to-handle-org-invitations branch from 64a2b55 to 1139076 Compare January 1, 2025 15:45
@rajpatel24 rajpatel24 self-assigned this Jan 1, 2025
@rajpatel24 rajpatel24 force-pushed the task-969-create-endpoints-to-handle-org-invitations branch from 1139076 to c28ad37 Compare January 2, 2025 14:20
@magicznyleszek
Copy link
Member

magicznyleszek commented Jan 4, 2025

@rajpatel24 Wuld that whole invite object be present in /api/v2/organizations/:organization_id/members/ in the invite property? And I see you have pending status in there, and is this the same as sent?

@rajpatel24
Copy link
Contributor Author

@rajpatel24 Wuld that whole invite object be present in /api/v2/organizations/:organization_id/members/ in the invite property? And I see you have pending status in there, and is this the same as sent?

@magicznyleszek No, the invite object would not be present there. I don't think it's necessary since /api/v2/organizations/:organization_id/members/ is designed to list the members who are already part of the organization - those who have accepted the invitations and are now part of it.

The pending status is the same as sent. When the organization owner sends an invitation, the status of the invite will be set to pending.

@rajpatel24 rajpatel24 force-pushed the task-969-create-endpoints-to-handle-org-invitations branch from 31ac077 to ea9a6bd Compare January 6, 2025 13:15
@rajpatel24 rajpatel24 force-pushed the task-969-create-endpoints-to-handle-org-invitations branch from 32b1431 to 5394358 Compare January 7, 2025 15:37
kobo/apps/organizations/serializers.py Show resolved Hide resolved
kobo/apps/organizations/tasks.py Outdated Show resolved Hide resolved
kobo/apps/organizations/models.py Show resolved Hide resolved

{% trans "All projects, submissions, data storage, transcription and translation usage for their projects will be transferred to you." %}

{% trans "Note: You will continue to have permissions to manage these projects until the user permissions are changed." %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this sentence should be here since that's the sender who receives this message.
(Please fix HTML template too)

kobo/apps/organizations/constants.py Outdated Show resolved Hide resolved
kobo/apps/organizations/models.py Outdated Show resolved Hide resolved
kobo/apps/organizations/models.py Outdated Show resolved Hide resolved
kobo/apps/organizations/permissions.py Show resolved Hide resolved
kobo/apps/organizations/tasks.py Outdated Show resolved Hide resolved
kobo/apps/organizations/views.py Show resolved Hide resolved
kobo/apps/organizations/views.py Show resolved Hide resolved
@@ -365,21 +376,26 @@ def send_invite_email(self):
email_message = EmailMessage(
to=to_email,
subject=t(
f"You're invited to join {organization_name}'s organization"
),
f"You're invited to join ##organization_name## organization"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not need to be an f-string anymore

sender_language = self.invited_by.extra_details.data.get(
'last_ui_language', 'en'
)
activate(sender_language)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems it would be better to use

with translation.override(sender_language):
    ...

instead of activate(sender_language)

Otherwise, everytime we use activate() we should restore the current language when we are done with translations

current_language = django.utils.translation.get_language()
activate(sender_language)
# do what you want with translations
activate(current_language)

Comment on lines 394 to 396
sender_language = self.invited_by.extra_details.data.get(
'last_ui_language', 'en'
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of hard-coding 'en', we should use either settings.LANGUAGE_CODE or django.utils.translation.get_language()

current_language = django.utils.translation.get_language()
# or current_language = settings.LANGUAGE_CODE
sender_language = self.invited_by.extra_details.data.get(
            'last_ui_language', current_language
)

f"You're invited to join {organization_name}'s organization"
),
f"You're invited to join ##organization_name## organization"
).replace('##organization_name##', organization_name),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use your utility replace_placeholders()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Also I have moved the utility function to a new file, as it is a generic utility not specific to the organizations app.

@@ -338,6 +340,7 @@ def _send_status_email(self, instance, status):
if email_func:
email_func()

@void_cache_for_request(keys=("organization",))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised the linter did not catch it but use single quotes 🙏 .

('owner', status.HTTP_200_OK),
('admin', status.HTTP_200_OK),
('member', status.HTTP_403_FORBIDDEN),
('external', status.HTTP_403_FORBIDDEN)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should 404 for external. We don't want to reveal the existence of the organization to external.

('owner', status.HTTP_200_OK),
('admin', status.HTTP_200_OK),
('member', status.HTTP_403_FORBIDDEN),
('external', status.HTTP_403_FORBIDDEN)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should 404 for external. We don't want to reveal the existence of the organization to external.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants