-
-
Notifications
You must be signed in to change notification settings - Fork 185
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
base: main
Are you sure you want to change the base?
feat(organizations): create endpoints to handle organization invitations #5395
Conversation
092d932
to
64a2b55
Compare
64a2b55
to
1139076
Compare
1139076
to
c28ad37
Compare
@rajpatel24 Wuld that whole invite object be present in |
@magicznyleszek No, the invite object would not be present there. I don't think it's necessary since The |
31ac077
to
ea9a6bd
Compare
32b1431
to
5394358
Compare
def create(self, validated_data): | ||
""" | ||
Create multiple invitations for the provided invitees. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move it before underscore prefixed methods.
# Get sender's organization without using the cached organization property, | ||
# as it may be outdated. | ||
sender_organization = Organization.objects.filter( | ||
organization_users__user=sender | ||
).first() | ||
recipient = sender_organization.owner_user_object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can it be outdated since the cache only lives during the request?
Can you provide some examples to reproduce the issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm updating the user's organization during the PATCH request (when the invitee accepts the invitation). However, after updating the user's organization, when I try to fetch the organization using the organization
property, it doesn't return the updated value. It's working fine if we remove the @cache_for_request
decorator. I've tried several ways to update the cached value but haven't been successful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, can you try to use void_cache_for_request()
decorator somewhere, somehow to update sender.organization
? Your solution does work but it means somewhere we could still call sender.organization
and organization
would not the correct one (until the request ends).
|
||
{% 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." %} |
There was a problem hiding this comment.
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)
@@ -0,0 +1,18 @@ | |||
{% load i18n %} | |||
{% trans "Hello," %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be a little more fancy,
If email has been sent to a username, let's use: Hello <username>
if email has been sent to a valid email with only one account, let's use: Hello <username>
if email has been sent to a valid email with more than one account, let's use: Hello <email>
.
Then get rid of (username: {{ recipient_username }})
in block below
What happens if invitation is sent to an email with multiple account? I don't see the template for that? The text is little bit different
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I've updated the template accordingly.
Regarding the template for multiple accounts, it is included in the same template with the has_multiple_accounts
condition. I’ve also updated some text for it.
…atuses from the model
@@ -1,4 +1,24 @@ | |||
INVITE_OWNER_ERROR = ( | |||
'This account is already the owner of {organization_name}. ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By convention, we are using ##placeholder##
to let the translators know it is a placeholder and it should not be translated.
As per our discussion, @rajpatel24 may create an utility function to replace placeholders (instead of calling .replace().replace().etc)
|
||
email_message = EmailMessage( | ||
to=self.invited_by.email, | ||
subject=t('KoboToolbox organization invitation accepted'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably need to call translation.activate(<language>)
to be sure the correct language is loaded.
sender_language = self.invited_by.extra_details.data.get('last_ui_language', DEFAULT_LANGUAGE)
translation.activate(sender_language)
As per our internal discussion:
last_ui_language
was used only for reports.
we should make sure it's clear in the code that we use that attribute for more than just reports
Please add a comment in ExtraUserDetail that it is also used to translate (email) templates in the user's language.
# Get recipient role with an article | ||
recipient_role = self.invitee_role | ||
if recipient_role and recipient_role[0].lower() in 'aeiou': | ||
recipient_role = 'an ' + recipient_role |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be the whole string to translate it.
recipient_role = t('an admin') if self.invitee_role === 'admin' else t('a member')
```
class OrgMembershipInvitePermission( | ||
ValidationPasswordPermissionMixin, IsAuthenticated | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During QA, anybody was able to see all invites from an org.
- Admins and owner should be able to see all their org invites
- Others should see all the invites related to them
Nobody should be able to see invites of other orgs.
# Get sender's organization without using the cached organization property, | ||
# as it may be outdated. | ||
sender_organization = Organization.objects.filter( | ||
organization_users__user=sender | ||
).first() | ||
recipient = sender_organization.owner_user_object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, can you try to use void_cache_for_request()
decorator somewhere, somehow to update sender.organization
? Your solution does work but it means somewhere we could still call sender.organization
and organization
would not the correct one (until the request ends).
self.client.force_login(user) | ||
return self.client.patch(self.detail_url(guid), data={'status': status}) | ||
|
||
def test_owner_can_send_invitation(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to test that owner and admins can send invitations but no one else. (member and anonymous).
self.assertEqual(response.data['status'], 'resent') | ||
self.assertEqual(mail.outbox[0].to[0], invitation.invitee.email) | ||
|
||
def test_owner_can_cancel_invitation(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment, we need to test every role just to be sure.
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(response.data['status'], 'cancelled') | ||
|
||
def test_list_invitations(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need also to test someone from OrganizationA cannot list invitations from OrganizationB (if they don't belong to it).
serializer_class = OrgMembershipInviteSerializer | ||
permission_classes = [OrgMembershipInvitePermission] | ||
http_method_names = ['get', 'post', 'patch', 'delete'] | ||
lookup_field = 'guid' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's see if we can use our KpiUidField (this comment should in the model :-P) instead of this guid
to be consistent with other fields. If cannot be replaced, no need to have two different fields which do the same thing.
'invitee', 'invited_by', 'organization' | ||
).filter(organization_id=organization_id) | ||
|
||
def create(self, request, *args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a->z :-)
🗒️ Checklist
<type>(<scope>)<!>: <title> TASK-1234
frontend
orbackend
unless it's global📣 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
member
,admin
). Default ismember
.Payload:
Response:
Response:
Payload:
Response:
Response: 204