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

Add ability to create, update project invitations #2430

Merged
merged 73 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
eaee418
add ProjectInvitation model
kelvin-muchiri May 17, 2023
629aef3
APIRequestFactory test
kelvin-muchiri May 19, 2023
65f88b3
add tests for get project invitations list
kelvin-muchiri May 19, 2023
6fa5243
add tests for get project invitations list
kelvin-muchiri May 22, 2023
803bc12
add create project invitation endpoint
kelvin-muchiri May 22, 2023
0e90f73
add tests for create project invitation endpoint
kelvin-muchiri May 23, 2023
1902839
update project invitation role
kelvin-muchiri May 24, 2023
2073af8
add endpoint to revoke project invitation
kelvin-muchiri May 25, 2023
6fa3d6f
add endpoint to resend project invitation
kelvin-muchiri May 25, 2023
cab218e
update comments
kelvin-muchiri May 25, 2023
5b9ddef
restore onadata/libs/filters.py
kelvin-muchiri May 25, 2023
bbd3c4e
make project invitation status readonly
kelvin-muchiri May 25, 2023
b8dee55
add project invitation endpoints documentation
kelvin-muchiri May 25, 2023
364d572
format project invitations documentation
kelvin-muchiri May 25, 2023
07030c5
format project invitations documentation
kelvin-muchiri May 25, 2023
b219c5e
format project invitations documentation
kelvin-muchiri May 25, 2023
b4f55a0
format project invitations documentation
kelvin-muchiri May 25, 2023
153811a
format project invitations documentation
kelvin-muchiri May 25, 2023
d55d0e9
update path for revoke, resend project invitation
kelvin-muchiri May 25, 2023
8262ccf
expose ProjectInvitation model to Django admin
kelvin-muchiri May 25, 2023
665647e
revert changes to expose ProjectInvitation in Django admin
kelvin-muchiri May 25, 2023
c1f74ff
fix lint errors
kelvin-muchiri May 26, 2023
e597fc8
fix lint errors
kelvin-muchiri May 26, 2023
aad57a9
fix cylic dependency
kelvin-muchiri May 26, 2023
dd4a833
fix linting errors
kelvin-muchiri May 26, 2023
910c730
refactor code
kelvin-muchiri Jun 2, 2023
ebd77dc
add code comments
kelvin-muchiri Jun 2, 2023
03bf45b
refactor code
kelvin-muchiri Jun 2, 2023
b61b9cb
suppress linting error
kelvin-muchiri Jun 2, 2023
f02675b
remove duplicate variable declaration
kelvin-muchiri Jun 2, 2023
0c58e8d
separate update project invitation from create
kelvin-muchiri Jun 2, 2023
5efa33f
add test case to update project invitation
kelvin-muchiri Jun 2, 2023
e21c996
fix typo in docs
kelvin-muchiri Jun 2, 2023
dd229f3
fix typo in docs
kelvin-muchiri Jun 2, 2023
21aeea3
Send and accept project invitation (#2443)
kelvin-muchiri Jun 26, 2023
03186b9
fix linting error
kelvin-muchiri Jun 26, 2023
bdd55f1
fix linting errors
kelvin-muchiri Jun 26, 2023
e2a7140
fix linting erros
kelvin-muchiri Jun 26, 2023
6e5e86f
fix linting erros
kelvin-muchiri Jun 26, 2023
7f41f35
fix linting errors
kelvin-muchiri Jun 26, 2023
99b1925
fix linting errors
kelvin-muchiri Jun 26, 2023
320ced2
fix linting errors
kelvin-muchiri Jun 26, 2023
d5440ae
fix linting errors
kelvin-muchiri Jun 26, 2023
eea06c5
fix linting errors
kelvin-muchiri Jun 26, 2023
f98d00b
Update invitations url path
KipSigei Jul 6, 2023
6678c2c
Fix typon in invitations endpoint methods
KipSigei Jul 6, 2023
3239765
Cleanup
KipSigei Jul 6, 2023
cafd130
remove HTML ampersand character from invitation mail
kelvin-muchiri Jul 11, 2023
d3cc026
remove unique together ProjectInvitation model
kelvin-muchiri Jul 11, 2023
6ccc7d9
refactor code
kelvin-muchiri Jul 11, 2023
011a4b3
add temporary logging for debugging
kelvin-muchiri Jul 12, 2023
697df79
log temporarily for debugging
kelvin-muchiri Jul 12, 2023
af19a1f
log temporarily for debugging
kelvin-muchiri Jul 12, 2023
e3c137a
log temp for debuggig
kelvin-muchiri Jul 12, 2023
c5c8dbb
remove debugging logs
kelvin-muchiri Jul 12, 2023
b582826
fix linting error
kelvin-muchiri Jul 12, 2023
e78e777
share projects if invitation invalid/missing
kelvin-muchiri Jul 12, 2023
98c6360
refactor code
kelvin-muchiri Jul 12, 2023
56c4ea4
fix failing test
kelvin-muchiri Jul 12, 2023
1f605a0
update documentatio
kelvin-muchiri Jul 12, 2023
23008ae
update documentation
kelvin-muchiri Jul 12, 2023
bf609fd
fix bug when working with multipart/formdata
kelvin-muchiri Jul 13, 2023
43f78c8
fix typo in docs
kelvin-muchiri Jul 13, 2023
efa179f
fix Invitation already exists when updating invitation
kelvin-muchiri Jul 13, 2023
a0044fa
fix 'User already exists' when updating an accepted invitation
kelvin-muchiri Jul 13, 2023
0d476b4
send project invtation email when email is updated
kelvin-muchiri Jul 13, 2023
183ea55
fix typo
kelvin-muchiri Jul 13, 2023
d79752f
Only accept project invitations whose email match new user email (#2449)
kelvin-muchiri Jul 13, 2023
5f1b9b5
fix linting errors
kelvin-muchiri Jul 13, 2023
7168ef3
fix error when creating user with no password
kelvin-muchiri Jul 14, 2023
9862cff
validate password if not None when creating user
kelvin-muchiri Jul 14, 2023
3794b97
refactor cod
kelvin-muchiri Jul 14, 2023
9fc5431
use queryset_iterator to iterate queryset
kelvin-muchiri Jul 20, 2023
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
237 changes: 237 additions & 0 deletions docs/projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Where:
- ``pk`` - is the project id
- ``formid`` - is the form id
- ``owner`` - is the username for the user or organization of the project
- ``invitation_pk`` - is the project invitation id

Register a new Project
-----------------------
Expand Down Expand Up @@ -515,3 +516,239 @@ Get user profiles that have starred a project

<pre class="prettyprint">
<b>GET</b> /api/v1/projects/<code>{pk}</code>/star</pre>

Get Project Invitation List
---------------------------

.. raw:: html

<pre class="prettyprint"><b>GET</b> /api/v1/projects/{pk}/invitations</pre>

Example
^^^^^^^

::

curl -X GET https://api.ona.io/api/v1/projects/1/invitations

Response
^^^^^^^^

::

[
{
"id": 1,
"email":"[email protected]",
"role":"readonly",
"status": 1

},
{
"id": 2,
"email":"[email protected]",
"role":"editor",
"status": 2,
}
]

Get a list of project invitations with a specific status
--------------------------------------------------------

The available choices are:

- ``1`` - Pending. Invitations which have not been accepted by recipients.
- ``2`` - Accepted. Invitations which have been accepted by recipients.
- ``3`` - Revoked. Invitations which were cancelled.


.. raw:: html

<pre class="prettyprint"><b>GET</b> /api/v1/projects/{pk}/invitations?status=2</pre>


Example
^^^^^^^

::

curl -X GET https://api.ona.io/api/v1/projects/1/invitations?status=2

Response
^^^^^^^^

::

[

{
"id": 2,
"email":"[email protected]",
"role":"editor",
"status": 2,
}
]


Create a new project invitation
-------------------------------

Invite an **unregistered** user to a project. An email will be sent to the user which has a link for them to
create an account.

.. raw:: html

<pre class="prettyprint"><b>POST</b> /api/v1/projects/{pk}/invitations</pre>

Example
^^^^^^^

::

curl -X POST -d "[email protected]" -d "role=readonly" https://api.ona.io/api/v1/projects/1/invitations


``email``: The email address of the unregistered user.

- Should be a valid email. If the ``PROJECT_INVITATION_EMAIL_DOMAIN_WHITELIST`` setting has been enabled, then the email domain has to be in the whitelist for it to be also valid

**Example**

::

PROJECT_INVITATION_EMAIL_DOMAIN_WHITELIST=["foo.com", "bar.com"]

- Email should not be that of a registered user

``role``: The user's role for the project.

- Must be a valid role


Response
^^^^^^^^

::

{
"id": 1,
"email": "[email protected]",
"role": "readonly",
"status": 1,
}


The link embedded in the email will be of the format ``http://{url}``
where:

- ``url`` - is the URL the recipient will be redirected to on clicking the link. The default is ``{domain}/api/v1/profiles`` where ``domain`` is domain where the API is hosted.

Normally, you would want the email recipient to be redirected to a web app. This can be achieved by
adding the setting ``PROJECT_INVITATION_URL``

**Example**

::

PROJECT_INVITATION_URL = 'https://example.com/register'


Update a project invitation
---------------------------

.. raw:: html

<pre class="prettyprint">
<b>PUT</b> /api/v1/projects/{pk}/invitations
</pre>


Example
^^^^^^^

::

curl -X PUT -d "[email protected]" -d "role=editor" -d "invitation_id=1" https://api.ona.io/api/v1/projects/1/invitations/1

Response
^^^^^^^^

::

{
"id": 1,
"email": "[email protected]",
"role": "editor",
"status": 1,
}


Resend a project invitation
---------------------------

Resend a project invitation email

.. raw:: html

<pre class="prettyprint"><b>POST</b> /api/v1/projects/{pk}/resend-invitation</pre>

Example
^^^^^^^

::

curl -X POST -d "invitation_id=6" https://api.ona.io/api/v1/projects/1/resend-invitation


``invitation_id``: The primary key of the ``ProjectInvitation`` to resend.

- Must be a ``ProjectInvitation`` whose status is **Pending**

Response
^^^^^^^^

::

{
"message": "Success"
}

Revoke a project invitation
---------------------------

Cancel a project invitation. A revoked invitation means that project will **not** be shared with the new user
even if they accept the invitation.

.. raw:: html

<pre class="prettyprint"><b>POST</b> /api/v1/projects/{pk}/revoke-invitation</pre>

Example
^^^^^^^

::

curl -X POST -d "invitation_id=6" https://api.ona.io/api/v1/projects/1/revoke-invitation

``invitation_id``: The primary key of the ``ProjectInvitation`` to resend.

- Must be a ``ProjectInvitation`` whose status is **Pending**

Response
^^^^^^^^

::

{
"message": "Success"
}


Accept a project invitation
---------------------------

Since a project invitation is sent to an unregistered user, acceptance of the invitation is handled
when `creating a new user <https://github.com/onaio/onadata/blob/main/docs/profiles.rst#register-a-new-user>`_.

All pending invitations whose email match the new user's email will be accepted and projects shared with the
user
9 changes: 6 additions & 3 deletions onadata/apps/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ def has_permission(self, request, view):
is_authenticated = request and request.user.is_authenticated

if is_authenticated and view.action == "create":

# Handle bulk create
# if doing a bulk create we will fail the entire process if the
# user lacks permissions for even one instance
Expand Down Expand Up @@ -278,6 +277,12 @@ def has_object_permission(self, request, view, obj):
if remove and request.user.username.lower() == username.lower():
return True

if view.action == "invitations" and not (
ManagerRole.user_has_role(request.user, obj)
or OwnerRole.user_has_role(request.user, obj)
):
return False

return super().has_object_permission(request, view, obj)


Expand Down Expand Up @@ -306,7 +311,6 @@ def has_permission(self, request, view):
and (request.user.is_authenticated or not self.authenticated_users_only)
and request.user.has_perms(perms)
):

return True

return False
Expand Down Expand Up @@ -387,7 +391,6 @@ class UserViewSetPermissions(DjangoModelPermissionsOrAnonReadOnly):
"""

def has_permission(self, request, view):

if request.user.is_anonymous and view.action == "list":
if request.GET.get("search"):
raise exceptions.NotAuthenticated()
Expand Down
20 changes: 19 additions & 1 deletion onadata/apps/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
import os
import sys
import logging
from datetime import timedelta

from celery.result import AsyncResult
Expand All @@ -17,7 +18,8 @@
from onadata.apps.api import tools
from onadata.libs.utils.email import send_generic_email
from onadata.libs.utils.model_tools import queryset_iterator
from onadata.apps.logger.models import Instance, XForm
from onadata.apps.logger.models import Instance, ProjectInvitation, XForm
from onadata.libs.utils.email import ProjectInvitationEmail
from onadata.celeryapp import app

User = get_user_model()
Expand Down Expand Up @@ -127,3 +129,19 @@ def delete_inactive_submissions():
for instance in queryset_iterator(instances):
# delete submission
instance.delete()


@app.task()
def send_project_invitation_email_async(
invitation_id: str, url: str
): # pylint: disable=invalid-name
"""Sends project invitation email asynchronously"""
try:
invitation = ProjectInvitation.objects.get(id=invitation_id)

except ProjectInvitation.DoesNotExist as err:
logging.exception(err)

else:
email = ProjectInvitationEmail(invitation, url)
email.send()
32 changes: 32 additions & 0 deletions onadata/apps/api/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Tests for module onadata.apps.api.tasks"""

from unittest.mock import patch

from onadata.apps.main.tests.test_base import TestBase
from onadata.apps.api.tasks import (
send_project_invitation_email_async,
)
from onadata.apps.logger.models import ProjectInvitation
from onadata.libs.utils.user_auth import get_user_default_project
from onadata.libs.utils.email import ProjectInvitationEmail


class SendProjectInivtationEmailAsyncTestCase(TestBase):
"""Tests for send_project_invitation_email_async"""

def setUp(self) -> None:
super().setUp()

project = get_user_default_project(self.user)
self.invitation = ProjectInvitation.objects.create(
project=project,
email="[email protected]",
role="manager",
)

@patch.object(ProjectInvitationEmail, "send")
def test_sends_email(self, mock_send):
"""Test email is sent"""
url = "https://example.com/register"
send_project_invitation_email_async(self.invitation.id, url)
mock_send.assert_called_once()
Loading