Skip to content

Commit

Permalink
Merge pull request #1060 from AI4Bharat/decentalized_approvals
Browse files Browse the repository at this point in the history
Decentalized approvals
  • Loading branch information
ishvindersethi22 authored Apr 30, 2024
2 parents cb18d21 + f580ef7 commit cb28801
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 1 deletion.
12 changes: 12 additions & 0 deletions backend/dataset/migrations/0046_merge_20240416_2233.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 3.2.14 on 2024-04-16 17:03

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("dataset", "0045_alter_ocrdocument_ocr_domain"),
("dataset", "0045_auto_20240321_0949"),
]

operations = []
34 changes: 34 additions & 0 deletions backend/users/migrations/0032_auto_20240417_1028.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.14 on 2024-04-17 04:58

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("users", "0031_user_notification_limit"),
]

operations = [
migrations.AddField(
model_name="user",
name="approved_by",
field=models.ForeignKey(
blank=True,
default=1,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="user",
name="is_approved",
field=models.BooleanField(
default=True,
help_text="Indicates whether user is approved by the admin or not.",
verbose_name="is_approved",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.14 on 2024-04-22 14:02

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("users", "0032_auto_20240417_1028"),
]

operations = [
migrations.RenameField(
model_name="user",
old_name="approved_by",
new_name="invited_by",
),
]
16 changes: 16 additions & 0 deletions backend/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ class User(AbstractBaseUser, PermissionsMixin):
"Indicates whether user prefers Chitralekha UI for audio transcription tasks or not."
),
)
# def get_default_user():
# return settings.AUTH_USER_MODEL.objects.get(id=1)

is_approved = models.BooleanField(
verbose_name="is_approved",
default=False,
help_text=("Indicates whether user is approved by the admin or not."),
)

invited_by = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
null=True,
blank=True,
default=1,
)

class Meta:
db_table = "user"
Expand Down
29 changes: 29 additions & 0 deletions backend/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@ def update(self, instance, validated_data):
return instance


class UsersPendingSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"role",
"invited_by",
"has_accepted_invite",
]

def update(self, instance, validated_data):
instance.id = validated_data.get("id", instance.id)
instance.username = validated_data.get("username", instance.username)
instance.first_name(validated_data.get("first_name", instance.first_name))
instance.last_name(validated_data.get("last_name", instance.last_name))
instance.email(validated_data.get("email", instance.email))
instance.role(validated_data.get("role", instance.role))
instance.invited_by(validated_data.get("invited_by", instance.invited_by))
instance.has_accepted_invite(
validated_data.get("has_accepted_invite", instance.has_accepted_invite)
)
instance.save()
return instance


class UserUpdateSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer()

Expand Down
185 changes: 184 additions & 1 deletion backend/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
UserLoginSerializer,
UserProfileSerializer,
UserSignUpSerializer,
UsersPendingSerializer,
UserUpdateSerializer,
LanguageSerializer,
ChangePasswordSerializer,
Expand Down Expand Up @@ -106,11 +107,15 @@ def invite_users(self, request):
organization_id=org.id,
role=request.data.get("role"),
)
user.is_approved = True
user.set_password(generate_random_string(10))
valid_user_emails.append(email)
users.append(user)
except:
pass
return Response(
{"message": "Error in creating user"},
status=status.HTTP_400_BAD_REQUEST,
)
else:
invalid_emails.append(email)
# setting error messages
Expand Down Expand Up @@ -258,6 +263,7 @@ def sign_up_user(self, request, pk=None):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
user.is_approved = False
return Response(
{"message": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
Expand All @@ -278,6 +284,183 @@ def sign_up_user(self, request, pk=None):
serialized.save()
return Response({"message": "User signed up"}, status=status.HTTP_200_OK)

# 1 add users to workspace - workspace name
# 2. Invite new users to {organisation name}
# function to list the users whose user.is_approved is false
@permission_classes([IsAuthenticated])
@swagger_auto_schema(responses={200: UsersPendingSerializer})
@action(detail=False, methods=["get"], url_path="pending_users")
def pending_users(self, request):
"""
List of users who have not accepted the invite yet in that organisation/workspace
"""
organisation_id = request.query_params.get("organisation_id")
users = User.objects.filter(organization_id=organisation_id, is_approved=False)
serialized = UsersPendingSerializer(users, many=True)

if serialized.data:
return Response(serialized.data, status=status.HTTP_200_OK)

return Response({"message": "No pending users"}, status=status.HTTP_200_OK)

# function to reject the user request to join the workspace by organiastion owner and delete the user from the table
@permission_classes([IsAuthenticated])
@is_organization_owner
@swagger_auto_schema(request_body=UsersPendingSerializer)
@action(detail=False, methods=["delete"], url_path="reject_user")
def reject_user(self, request):
"""
Reject the user request to join the workspace
"""
try:
user_id = request.query_params.get("userId", None)
user = User.objects.get(id=user_id)

if user.is_approved == True:
return Response(
{"message": "User is already approved"},
status=status.HTTP_400_BAD_REQUEST,
)
except User.DoesNotExist:
return Response(
{"message": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
user.delete()
return Response({"message": "User rejected"}, status=status.HTTP_200_OK)

# function to approve the user request to join the workspace by organiastion owner and update the user.is_approved to true
@permission_classes([IsAuthenticated])
@is_organization_owner
@swagger_auto_schema(request_body=UsersPendingSerializer)
@action(detail=False, methods=["post"], url_path="approve_user")
def approve_user(self, request):
"""
Approve the user request to join the workspace
"""
try:
user_id = request.query_params.get("userId", None)
user = User.objects.get(id=user_id)
organisation_id = user.organization_id

try:
organisation = Organization.objects.get(id=organisation_id)
except Organization.DoesNotExist:
return Response(
{"message": "Organization not found"},
status=status.HTTP_404_NOT_FOUND,
)
if user.is_approved == True:
return Response(
{"message": "User is already approved"},
status=status.HTTP_400_BAD_REQUEST,
)
user.is_approved = True
user.save()
# invite the user via mail now
try:
users = []
users.append(user)
Invite.create_invite(organization=organisation, users=users)
except Exception as e:
return Response(
{"message": f"Error in sending invite: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
except User.DoesNotExist:
return Response(
{"message": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
return Response({"message": "User approved"}, status=status.HTTP_200_OK)

# function to request workspace owner to add the users to the workspace by workspace manager
@permission_classes([IsAuthenticated])
@swagger_auto_schema(request_body=InviteGenerationSerializer)
@action(detail=False, methods=["post"], url_path="request_user")
def request_user(self, request):
"""
Request the workspace owner to add the user to the workspace from manager
"""
all_emails = request.data.get("emails")
distinct_emails = list(set(all_emails))
organization_id = request.data.get("organization_id")
users = []

try:
org = Organization.objects.get(id=organization_id)
except Organization.DoesNotExist:
return Response(
{"message": "Organization not found"}, status=status.HTTP_404_NOT_FOUND
)
already_existing_emails = []
valid_user_emails = []
invalid_emails = []
existing_emails_set = set(Invite.objects.values_list("user__email", flat=True))

for email in distinct_emails:
# Checking if the email is in valid format.
if re.fullmatch(regex, email):
if email in existing_emails_set:
already_existing_emails.append(email)
continue
try:
user = User(
username=generate_random_string(12),
email=email.lower(),
organization_id=org.id,
role=request.data.get("role"),
has_accepted_invite=False,
is_approved=False,
)
user.set_password(generate_random_string(10))
valid_user_emails.append(email)
users.append(user)
except:
pass
else:
invalid_emails.append(email)
# setting error messages
(
additional_message_for_existing_emails,
additional_message_for_invalid_emails,
) = ("", "")
additional_message_for_valid_emails = ""
if already_existing_emails:
additional_message_for_existing_emails += (
f", Invites already sent to: {','.join(already_existing_emails)}"
)
if invalid_emails:
additional_message_for_invalid_emails += (
f", Invalid emails: {','.join(invalid_emails)}"
)
if valid_user_emails:
additional_message_for_valid_emails += (
f", Invites sent to : {','.join(valid_user_emails)}"
)
if len(valid_user_emails) == 0:
return Response(
{
"message": "No invites sent"
+ additional_message_for_invalid_emails
+ additional_message_for_existing_emails
},
status=status.HTTP_400_BAD_REQUEST,
)
elif len(invalid_emails) == 0:
ret_dict = {
"message": "Invites sent & request sent to workspace owner"
+ additional_message_for_valid_emails
+ additional_message_for_existing_emails
}
else:
ret_dict = {
"message": f"Invites sent partially!"
+ additional_message_for_valid_emails
+ additional_message_for_invalid_emails
+ additional_message_for_existing_emails
}
users = User.objects.bulk_create(users)
return Response(ret_dict, status=status.HTTP_201_CREATED)


class AuthViewSet(viewsets.ViewSet):
@permission_classes([AllowAny])
Expand Down

0 comments on commit cb28801

Please sign in to comment.