diff --git a/backend/dataset/migrations/0046_merge_20240416_2233.py b/backend/dataset/migrations/0046_merge_20240416_2233.py
new file mode 100644
index 000000000..24617f1e7
--- /dev/null
+++ b/backend/dataset/migrations/0046_merge_20240416_2233.py
@@ -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 = []
diff --git a/backend/organizations/models.py b/backend/organizations/models.py
index afa16929c..e8a37f439 100644
--- a/backend/organizations/models.py
+++ b/backend/organizations/models.py
@@ -4,10 +4,11 @@
from shoonya_backend.settings import AUTH_USER_MODEL
from shoonya_backend.mixins import DummyModelMixin
import secrets
-from django.core.mail import send_mail
+from django.core.mail import EmailMultiAlternatives
import os
from dotenv import load_dotenv
+
load_dotenv()
from django.conf import settings
@@ -120,6 +121,139 @@ def __str__(self):
+ " organization"
)
+ @classmethod
+ def send_invite_email(cls, invite, user):
+ current_environment = os.getenv("ENV")
+ base_url = (
+ "dev.shoonya.ai4bharat.org"
+ if current_environment == "dev"
+ else "shoonya.ai4bharat.org"
+ )
+ subject = "Invitation to join Organization"
+ invite_link = f"https://{base_url}/#/invite/{invite.invite_code}"
+ text_content = f"Hello! You are invited to Shoonya. Your Invite link is: "
+ style_string = """
+ *{ margin: 0;
+ padding: 0;
+ }
+ body {
+ font-family: "Arial", sans-serif;
+ background-color: #f2f8f8;
+ margin: 0;
+ padding: 0;
+ padding-top: 2rem;
+ }
+ .container {
+ background-color: #fff;
+ border: solid 1px #e1e1e1;
+ border-radius: 2px;
+ padding: 1.4rem;
+ max-width: 380px;
+ margin: auto;
+ }
+ .header {
+ width: fit-content;
+ margin: auto;
+ }
+ h1 {
+ font-size: 1.2rem;
+ font-weight: 300;
+ margin: 1rem 0;
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ }
+ p {
+ font-size: 0.9rem;
+ color: #222;
+ margin: 0.8rem 0;
+ }
+ .primary {
+ color: #18621f;
+ }
+ .footer {
+ margin-top: 1rem;
+ font-size: 0.9rem;
+ }
+ .footer > * {
+ font-size: inherit;
+ }"""
+
+ html_content = f"""
+
+
+
+
+
+ Invitation to join Shoonya Organisation
+
+
+
+
+
+
+
+
+
+ Please use the above link to verify your email address and complete your registration.
+
+
+ For security purposes, please do not share the this link with
+ anyone.
+
+
+ If clicking the link doesn't work, you can copy and paste the link into your browser's address window, or retype it there.
+ {invite_link}
+
+
+
+
+
+
+
+ """
+ msg = EmailMultiAlternatives(
+ subject, text_content, settings.DEFAULT_FROM_EMAIL, [user.email]
+ )
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+
@classmethod
def create_invite(cls, organization=None, users=None):
with transaction.atomic():
@@ -130,12 +264,7 @@ def create_invite(cls, organization=None, users=None):
invite = Invite.objects.create(organization=organization, user=user)
invite.invite_code = cls.generate_invite_code()
invite.save()
- send_mail(
- "Invitation to join Organization",
- f"Hello! You are invited to {organization.title}. Your Invite link is: https://shoonya.ai4bharat.org/#/invite/{invite.invite_code}",
- settings.DEFAULT_FROM_EMAIL,
- [user.email],
- )
+ cls.send_invite_email(invite, user)
# def has_permission(self, user):
# if self.organization.created_by.pk == user.pk or user.is_superuser:
@@ -147,12 +276,7 @@ def re_invite(cls, users=None):
with transaction.atomic():
for user in users:
invite = Invite.objects.get(user=user)
- send_mail(
- "Invitation to join Organization",
- f"Hello! You are invited to {invite.organization.title}. Your Invite link is: https://shoonya.ai4bharat.org/#/invite/{invite.invite_code}",
- settings.DEFAULT_FROM_EMAIL,
- [user.email],
- )
+ cls.send_invite_email(invite, user)
@classmethod
def generate_invite_code(cls):
diff --git a/backend/tasks/views.py b/backend/tasks/views.py
index 43517ac06..50de59c54 100644
--- a/backend/tasks/views.py
+++ b/backend/tasks/views.py
@@ -1740,15 +1740,6 @@ def partial_update(self, request, pk=None):
if annotation_obj.annotation_status == TO_BE_REVISED:
update_notification(annotation_obj, task)
is_revised = True
- print(annotation_obj)
- if "ids" in dict(request.data):
- pass
-
- else:
- return Response(
- {"message": "key doesnot match"},
- status=status.HTTP_400_BAD_REQUEST,
- )
elif annotation_obj.annotation_type == SUPER_CHECKER_ANNOTATION:
is_rejected = False
diff --git a/backend/users/migrations/0032_auto_20240417_1028.py b/backend/users/migrations/0032_auto_20240417_1028.py
new file mode 100644
index 000000000..5bbd87b9e
--- /dev/null
+++ b/backend/users/migrations/0032_auto_20240417_1028.py
@@ -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",
+ ),
+ ),
+ ]
diff --git a/backend/users/migrations/0033_rename_approved_by_user_invited_by.py b/backend/users/migrations/0033_rename_approved_by_user_invited_by.py
new file mode 100644
index 000000000..afafab796
--- /dev/null
+++ b/backend/users/migrations/0033_rename_approved_by_user_invited_by.py
@@ -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",
+ ),
+ ]
diff --git a/backend/users/models.py b/backend/users/models.py
index 1dff1cb2a..c391d5f23 100644
--- a/backend/users/models.py
+++ b/backend/users/models.py
@@ -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"
diff --git a/backend/users/serializers.py b/backend/users/serializers.py
index 414388300..98048b15a 100644
--- a/backend/users/serializers.py
+++ b/backend/users/serializers.py
@@ -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()
diff --git a/backend/users/views.py b/backend/users/views.py
index b02aeb403..320a06901 100644
--- a/backend/users/views.py
+++ b/backend/users/views.py
@@ -20,6 +20,7 @@
UserLoginSerializer,
UserProfileSerializer,
UserSignUpSerializer,
+ UsersPendingSerializer,
UserUpdateSerializer,
LanguageSerializer,
ChangePasswordSerializer,
@@ -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
@@ -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
)
@@ -278,6 +284,190 @@ 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)
+
+ # demo_user = User.objects.filter(id=1)
+ # filtered_user = demo_user.values_list("email", flat=True)
+ # # Convert QuerySet to list and get first element
+ # email = list(filtered_user)[0]
+ # print(email)
+ # print(request.user)
+ 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", Requested users : {','.join(valid_user_emails)}"
+ )
+ if len(valid_user_emails) == 0:
+ return Response(
+ {
+ "message": "No Requests 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": "The invites to this users will be sent after approval from the organization owner"
+ + additional_message_for_valid_emails
+ + additional_message_for_existing_emails
+ }
+ else:
+ ret_dict = {
+ "message": f"Request 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])