Skip to content
Open
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
68 changes: 63 additions & 5 deletions onadata/apps/api/tests/viewsets/test_user_profile_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
from onadata.apps.api.tests.viewsets.test_abstract_viewset import TestAbstractViewSet
from onadata.apps.api.viewsets.connect_viewset import ConnectViewSet
from onadata.apps.api.viewsets.user_profile_viewset import UserProfileViewSet
from onadata.apps.messaging.constants import (
USER, USER_CREATED, USER_UPDATED, USER_PASSWORD_CHANGED
)
from onadata.apps.logger.models.instance import Instance
from onadata.apps.logger.models.project_invitation import ProjectInvitation
from onadata.apps.main.models import UserProfile
Expand Down Expand Up @@ -252,13 +255,14 @@ def test_profiles_get_org_anon(self):

@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
@override_settings(ENABLE_EMAIL_VERIFICATION=True)
@patch("onadata.libs.serializers.user_profile_serializer.send_message")
@patch(
(
"onadata.libs.serializers.user_profile_serializer."
"send_verification_email.delay"
)
)
def test_profile_create(self, mock_send_verification_email):
def test_profile_create(self, mock_send_verification_email, mock_send_message):
request = self.factory.get("/", **self.extra)
response = self.view(request)
self.assertEqual(response.status_code, 200)
Expand Down Expand Up @@ -290,6 +294,16 @@ def test_profile_create(self, mock_send_verification_email):
self.assertTrue(user.is_active)
self.assertTrue(user.check_password(password), password)

# Save on user creation event
self.assertTrue(mock_send_message.called)
mock_send_message.assert_called_with(
instance_id=user.username,
target_id=user.id,
target_type=USER,
user=user,
message_verb=USER_CREATED
)

def _create_user_using_profiles_endpoint(self, data):
request = self.factory.post(
"/api/v1/profiles",
Expand Down Expand Up @@ -572,7 +586,8 @@ def test_split_long_name_to_first_name_and_last_name(self):
self.assertEqual(first_name, "(CPLTGL) Centre Pour la Promot")
self.assertEqual(last_name, "ion de la Liberte D'Expression")

def test_partial_updates(self):
@patch("onadata.apps.api.viewsets.user_profile_viewset.send_message")
def test_partial_updates(self, mock_send_message):
self.assertEqual(self.user.profile.country, "US")
country = "KE"
username = "george"
Expand All @@ -587,7 +602,18 @@ def test_partial_updates(self):
self.assertEqual(profile.metadata, metadata)
self.assertEqual(profile.user.username, username)

def test_partial_updates_empty_metadata(self):
# Save on user update (Patch) event
self.assertTrue(mock_send_message.called)
mock_send_message.assert_called_with(
instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_UPDATED
)

@patch("onadata.apps.api.viewsets.user_profile_viewset.send_message")
def test_partial_updates_empty_metadata(self, mock_send_message):
profile = UserProfile.objects.get(user=self.user)
profile.metadata = {}
profile.save()
Expand All @@ -600,6 +626,16 @@ def test_partial_updates_empty_metadata(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(profile.metadata, metadata)

# Save on user update (Patch) event
self.assertTrue(mock_send_message.called)
mock_send_message.assert_called_with(
instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_UPDATED
)

def test_partial_updates_too_long(self):
# the max field length for username is 30 in django
username = "a" * 31
Expand Down Expand Up @@ -678,7 +714,8 @@ def test_partial_update_metadata_field(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(profile.metadata, {"b": "caah"})

def test_put_update(self):
@patch("onadata.apps.api.viewsets.user_profile_viewset.send_message")
def test_put_update(self, mock_send_message):
data = _profile_data()
# create profile
request = self.factory.post(
Expand Down Expand Up @@ -715,6 +752,16 @@ def test_put_update(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["city"], data["city"])

# Save on user update (Put) event
self.assertTrue(mock_send_message.called)
mock_send_message.assert_called_with(
instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_UPDATED
)

def test_profile_create_mixed_case(self):
request = self.factory.get("/", **self.extra)
response = self.view(request)
Expand Down Expand Up @@ -757,7 +804,8 @@ def test_profile_create_mixed_case(self):
{"NAME": "onadata.libs.utils.validators.PreviousPasswordValidator"}
]
)
def test_change_password(self):
@patch("onadata.apps.api.viewsets.user_profile_viewset.send_message")
def test_change_password(self, mock_send_message):
view = UserProfileViewSet.as_view({"post": "change_password"})
current_password = "bobbob"
new_password = "bobbob1"
Expand Down Expand Up @@ -822,6 +870,16 @@ def test_change_password(self):
self.assertNotEqual(response.data["access_token"], old_token)
new_token = response.data["access_token"]

# Save password change event
self.assertTrue(mock_send_message.called)
mock_send_message.assert_called_with(
instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_PASSWORD_CHANGED
)

# Assert requests made with the old tokens are rejected
post_data = {"current_password": new_password, "new_password": "random"}
request = self.factory.post("/", data=post_data, **self.extra)
Expand Down
37 changes: 37 additions & 0 deletions onadata/apps/api/viewsets/user_profile_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core.cache import cache
from django.core.validators import ValidationError
Expand All @@ -32,6 +33,10 @@
from onadata.apps.api.tools import get_baseviewset_class
from onadata.apps.logger.models.instance import Instance
from onadata.apps.main.models import UserProfile
from onadata.apps.messaging.constants import (
USER, USER_UPDATED, USER_PASSWORD_CHANGED
)
from onadata.apps.messaging.serializers import send_message
from onadata.libs import filters
from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin
from onadata.libs.mixins.cache_control_mixin import CacheControlMixin
Expand All @@ -50,6 +55,9 @@
from onadata.libs.utils.email import get_verification_email_data, get_verification_url
from onadata.libs.utils.user_auth import invalidate_and_regen_tokens

# pylint: disable=invalid-name
User = get_user_model()

BaseViewset = get_baseviewset_class() # pylint: disable=invalid-name
LOCKOUT_TIME = getattr(settings, "LOCKOUT_TIME", 1800)
MAX_CHANGE_PASSWORD_ATTEMPTS = getattr(settings, "MAX_CHANGE_PASSWORD_ATTEMPTS", 10)
Expand Down Expand Up @@ -211,6 +219,16 @@ def update(self, request, *args, **kwargs):
username = kwargs.get("user")
response = super().update(request, *args, **kwargs)
cache.set(f"{USER_PROFILE_PREFIX}{username}", response.data)

# send notification on updating user profile
send_message(
Copy link
Contributor

Choose a reason for hiding this comment

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

We can also move this to the serializer. Since we are sending the messaging for both create and update, we can override the serializer's save method and call send_message there. Example

We can differentiate create from update by checking self.instance. When doing an update self.instance is not None

instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_UPDATED,
)

return response

def retrieve(self, request, *args, **kwargs):
Expand Down Expand Up @@ -283,6 +301,15 @@ def change_password(self, request, *args, **kwargs): # noqa
user_profile.save()
data.update(invalidate_and_regen_tokens(user=user_profile.user))

# send notification account password change
send_message(
instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_PASSWORD_CHANGED,
)

return Response(status=status.HTTP_200_OK, data=data)

def partial_update(self, request, *args, **kwargs):
Expand All @@ -303,6 +330,16 @@ def partial_update(self, request, *args, **kwargs):

profile.metadata = metadata
profile.save()

# send notification on updating user profile
send_message(
Copy link
Contributor

@kelvin-muchiri kelvin-muchiri Sep 6, 2024

Choose a reason for hiding this comment

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

partial_update will call the serializer's save method. If we refactor to send the message for update within the serializer, then we can get rid of sending the message here

instance_id=request.user.username,
target_id=request.user.id,
target_type=USER,
user=request.user,
message_verb=USER_UPDATED,
)

return Response(data=profile.metadata, status=status.HTTP_200_OK)

return super().partial_update(request, *args, **kwargs)
Expand Down
11 changes: 9 additions & 2 deletions onadata/apps/messaging/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@
SUBMISSION_DELETED = "submission_deleted"
SUBMISSION_REVIEWED = "submission_reviewed"
FORM_UPDATED = "form_updated"
USER_CREATED = "user_created"
USER_UPDATED = "user_updated"
USER_PASSWORD_CHANGED = "password_changed" # nosec # noqa
MESSAGE_VERBS = [
MESSAGE, SUBMISSION_REVIEWED, SUBMISSION_CREATED, SUBMISSION_EDITED,
SUBMISSION_DELETED, FORM_UPDATED]
SUBMISSION_DELETED, FORM_UPDATED, USER_CREATED, USER_UPDATED,
USER_PASSWORD_CHANGED]
VERB_TOPIC_DICT = {
SUBMISSION_CREATED: "submission/created",
SUBMISSION_EDITED: "submission/edited",
SUBMISSION_DELETED: "submission/deleted",
SUBMISSION_REVIEWED: "submission/reviewed",
FORM_UPDATED: "form/updated"
FORM_UPDATED: "form/updated",
USER_CREATED: "user/created",
USER_UPDATED: "user/updated",
USER_PASSWORD_CHANGED: "user/password_changed"
}
11 changes: 11 additions & 0 deletions onadata/libs/serializers/user_profile_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
)
from onadata.apps.main.forms import RegistrationFormUserProfile
from onadata.apps.main.models import UserProfile
from onadata.apps.messaging.constants import USER, USER_CREATED
from onadata.apps.messaging.serializers import send_message
from onadata.libs.authentication import expired
from onadata.libs.permissions import CAN_VIEW_PROFILE, is_organization
from onadata.libs.serializers.fields.json_field import JsonField
Expand Down Expand Up @@ -319,6 +321,15 @@ def create(self, validated_data):
)
profile.save()

# send notification on creating new user account
send_message(
instance_id=new_user.username,
target_id=new_user.id,
target_type=USER,
user=new_user,
message_verb=USER_CREATED,
)

if getattr(settings, "ENABLE_EMAIL_VERIFICATION", False):
redirect_url = params.get("redirect_url")
_send_verification_email(redirect_url, new_user, request)
Expand Down