diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index e69de29bb..63c59b210 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +_all_ = ('celery_app',) \ No newline at end of file diff --git a/backend/backend/celery.py b/backend/backend/celery.py new file mode 100644 index 000000000..badc78c56 --- /dev/null +++ b/backend/backend/celery.py @@ -0,0 +1,12 @@ +# backend/celery.py + +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') + +app = Celery('backend') + +app.config_from_object('django.conf:settings', namespace='CELERY') + +app.autodiscover_tasks() \ No newline at end of file diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 9de4f024a..e909c5a32 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -24,8 +24,9 @@ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] -FRONTEND_URL = os.environ["FRONTEND_URL"] +from decouple import config +SECRET_KEY = config("DJANGO_SECRET_KEY") +FRONTEND_URL = config("FRONTEND_URL",default='http://127.0.0.1:3000') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -47,8 +48,18 @@ "authentication", "chat", "gpt", + 'django_filters', + ] +# task 3 +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} + + MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -84,13 +95,27 @@ # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases +# DATABASES = { +# "default": { +# "ENGINE": "django.db.backends.sqlite3", +# "NAME": BASE_DIR / "db.sqlite3", +# } +# } + +# settings.py + DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'mydatabase', # Database name + 'USER': 'postgres', # e.g., postgres + 'PASSWORD': 'Dhanush@2002', + 'HOST': 'localhost', # Or IP address if remote + 'PORT': '5432', # Default PostgreSQL port } } + # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators @@ -149,3 +174,11 @@ SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True CSRF_COOKIE_SAMESITE = "None" + +# CELERY SETTINGS +CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' + +# CELERY BEAT SETTINGS +INSTALLED_APPS += ['django_celery_beat'] diff --git a/backend/backend/urls.py b/backend/backend/urls.py index fa154c7fb..4b4abecd4 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -17,4 +17,6 @@ def root_view(request): path("gpt/", include("gpt.urls")), path("auth/", include("authentication.urls")), path("", root_view), + # task-3 + path('api/', include('chat.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/backend/chat/admin.py b/backend/chat/admin.py index a4e7d15fc..01de95837 100644 --- a/backend/chat/admin.py +++ b/backend/chat/admin.py @@ -3,6 +3,11 @@ from nested_admin.nested import NestedModelAdmin, NestedStackedInline, NestedTabularInline from chat.models import Conversation, Message, Role, Version +# task-3 +from .models import FileUpload +from django import forms +from django.contrib import messages +from django.core.exceptions import ValidationError class RoleAdmin(NestedModelAdmin): @@ -51,7 +56,7 @@ def queryset(self, request, queryset): class ConversationAdmin(NestedModelAdmin): actions = ["undelete_selected", "soft_delete_selected"] inlines = [VersionInline] - list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user") + list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user","summary") list_filter = (DeletedListFilter,) ordering = ("-modified_at",) @@ -80,6 +85,30 @@ def is_deleted(self, obj): is_deleted.boolean = True is_deleted.short_description = "Deleted?" +# task-3 upload file +# Custom form to show only user and file (hide hash/size in input) +class FileUploadForm(forms.ModelForm): + class Meta: + model = FileUpload + fields = ['user', 'file'] # only show user and file fields +@admin.register(FileUpload) +class FileUploadAdmin(admin.ModelAdmin): + form = FileUploadForm + list_display = ("id", "file_name", "file_size", "file_hash", "uploaded_at", "user") + readonly_fields = ("file_name", "file_size", "file_hash", "uploaded_at") + + def save_model(self, request, obj, form, change): + try: + obj.save() # Triggers full_clean() with validation + self.message_user(request, "File uploaded successfully.", level=messages.SUCCESS) + except ValidationError as e: + self.message_user( + request, + f"Upload failed: {e.messages[0]}", + level=messages.WARNING + ) + + class VersionAdmin(NestedModelAdmin): inlines = [MessageInline] diff --git a/backend/chat/apps.py b/backend/chat/apps.py index 5f75238d2..0fe5d5277 100644 --- a/backend/chat/apps.py +++ b/backend/chat/apps.py @@ -1,6 +1,12 @@ from django.apps import AppConfig + + class ChatConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "chat" + default_auto_field = 'django.db.models.BigAutoField' + name = 'chat' + + def ready(self): + import chat.signals + diff --git a/backend/chat/management/commands/cleanup_old_conversations.py b/backend/chat/management/commands/cleanup_old_conversations.py new file mode 100644 index 000000000..c9d7d0b6a --- /dev/null +++ b/backend/chat/management/commands/cleanup_old_conversations.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from chat.models import Conversation +from datetime import timedelta + +class Command(BaseCommand): + help = 'Soft-deletes conversations older than 30 days' + + def handle(self, *args, **kwargs): + days = 30 + cutoff_date = timezone.now() - timedelta(days=days) + + deleted_count = Conversation.objects.filter( + created_at__lt=cutoff_date, + deleted_at__isnull=True + ).update(deleted_at=timezone.now()) + + self.stdout.write( + self.style.SUCCESS(f"{deleted_count} conversations soft-deleted.") + ) \ No newline at end of file diff --git a/backend/chat/migrations/0002_conversation_summary.py b/backend/chat/migrations/0002_conversation_summary.py new file mode 100644 index 000000000..ecea70f22 --- /dev/null +++ b/backend/chat/migrations/0002_conversation_summary.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-07-05 09:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='summary', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/backend/chat/migrations/0003_uploadedfile.py b/backend/chat/migrations/0003_uploadedfile.py new file mode 100644 index 000000000..18101d67d --- /dev/null +++ b/backend/chat/migrations/0003_uploadedfile.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-07-06 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0002_conversation_summary'), + ] + + operations = [ + migrations.CreateModel( + name='UploadedFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='uploads/')), + ('filename', models.CharField(max_length=255)), + ('checksum', models.CharField(max_length=64)), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/backend/chat/migrations/0004_fileupload_delete_uploadedfile_and_more.py b/backend/chat/migrations/0004_fileupload_delete_uploadedfile_and_more.py new file mode 100644 index 000000000..080581676 --- /dev/null +++ b/backend/chat/migrations/0004_fileupload_delete_uploadedfile_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.4 on 2025-07-07 19:39 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0003_uploadedfile'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FileUpload', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='uploads/')), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('file_name', models.CharField(max_length=255)), + ('file_size', models.PositiveIntegerField()), + ('file_hash', models.CharField(max_length=64)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.DeleteModel( + name='UploadedFile', + ), + migrations.AddConstraint( + model_name='fileupload', + constraint=models.UniqueConstraint(fields=('user', 'file_hash'), name='unique_user_file_hash'), + ), + ] diff --git a/backend/chat/migrations/0005_filelog.py b/backend/chat/migrations/0005_filelog.py new file mode 100644 index 000000000..5b68424dc --- /dev/null +++ b/backend/chat/migrations/0005_filelog.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.4 on 2025-07-08 11:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0004_fileupload_delete_uploadedfile_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FileLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file_name', models.CharField(max_length=255)), + ('action', models.CharField(choices=[('UPLOAD', 'Upload'), ('DELETE', 'Delete'), ('ACCESS', 'Access')], max_length=10)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/chat/migrations/0006_books.py b/backend/chat/migrations/0006_books.py new file mode 100644 index 000000000..e21d8eab8 --- /dev/null +++ b/backend/chat/migrations/0006_books.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-07-08 12:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0005_filelog'), + ] + + operations = [ + migrations.CreateModel( + name='Books', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('book_name', models.CharField(max_length=100)), + ('author', models.CharField(max_length=30)), + ], + ), + ] diff --git a/backend/chat/models.py b/backend/chat/models.py index 242788f14..c563ce1bf 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -3,6 +3,12 @@ from django.db import models from authentication.models import CustomUser +# task-3 +import hashlib +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +import hashlib +from django.contrib.auth.models import User class Role(models.Model): @@ -22,6 +28,7 @@ class Conversation(models.Model): ) deleted_at = models.DateTimeField(null=True, blank=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + summary = models.TextField(blank=True, null=True) def __str__(self): return self.title @@ -32,6 +39,48 @@ def version_count(self): version_count.short_description = "Number of versions" +# task-3 file uploadwith duplicate check +User = get_user_model() + +class FileUpload(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + file = models.FileField(upload_to='uploads/') + uploaded_at = models.DateTimeField(auto_now_add=True) + file_name = models.CharField(max_length=255) + file_size = models.PositiveIntegerField() + file_hash = models.CharField(max_length=64) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['user', 'file_hash'], name='unique_user_file_hash') + ] + + def clean(self): + # Check for duplicate files for the same user + if FileUpload.objects.filter(user=self.user, file_hash=self.file_hash).exclude(pk=self.pk).exists(): + raise ValidationError("Duplicate file. Already uploaded by this user.") + + def save(self, *args, **kwargs): + # Only calculate hash if not already set + if not self.file_hash: + hasher = hashlib.sha256() + for chunk in self.file.chunks(): + hasher.update(chunk) + self.file_hash = hasher.hexdigest() + + # Set file name and size automatically + self.file_name = self.file.name + self.file_size = self.file.size + + # Run validation and save + self.full_clean() # Calls clean() + super().save(*args, **kwargs) + + def _str_(self): + return f"{self.user.email} - {self.file_name}" + + + class Version(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) conversation = models.ForeignKey("Conversation", related_name="versions", on_delete=models.CASCADE) @@ -63,3 +112,22 @@ def save(self, *args, **kwargs): def __str__(self): return f"{self.role}: {self.content[:20]}..." + +class FileLog(models.Model): + ACTION_CHOICES= [ + ('UPLOAD', 'Upload'), + ('DELETE', 'Delete'), + ('ACCESS', 'Access'), +] + user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True) + file_name = models.CharField(max_length=255) + action = models.CharField(max_length=10, choices=ACTION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + + def _str_(self): + return f"{self.timestamp} - {self.user} - {self.action} - {self.file_name}" + + + + + \ No newline at end of file diff --git a/backend/chat/serializers.py b/backend/chat/serializers.py index 0c721c061..102a73abb 100644 --- a/backend/chat/serializers.py +++ b/backend/chat/serializers.py @@ -1,6 +1,9 @@ from django.core.exceptions import ValidationError from django.utils import timezone +# task-3 file upload with duplicate check +import hashlib from rest_framework import serializers +from .models import FileUpload from chat.models import Conversation, Message, Role, Version @@ -8,6 +11,31 @@ def should_serialize(validated_data, field_name) -> bool: if validated_data.get(field_name) is not None: return True + +# Task -3 changes +class ConversationSummarySerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ['id', 'title', 'summary', 'created_at'] + +# task-3 file upload with duplicate check +class UploadedFileSerializer(serializers.ModelSerializer): + class Meta: + model = FileUpload + fields = '__all__' + + def create(self, validated_data): + file = validated_data['file'] + content = file.read() + file.seek(0) + checksum = hashlib.sha256(content).hexdigest() + + if FileUpload.objects.filter(checksum=checksum).exists(): + raise serializers.ValidationError("Duplicate file detected.") + + validated_data['checksum'] = checksum + validated_data['filename'] = file.name + return super().create(validated_data) class TitleSerializer(serializers.Serializer): diff --git a/backend/chat/signals.py b/backend/chat/signals.py new file mode 100644 index 000000000..5e5d82852 --- /dev/null +++ b/backend/chat/signals.py @@ -0,0 +1,28 @@ +# chat/signals.py +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Conversation, Message +import openai + +@receiver(post_save, sender=Conversation) +def generate_summary(sender, instance, created, **kwargs): + if created or not instance.summary: + messages = Message.objects.filter(conversation=instance).order_by('created_at') + text = "\n".join([msg.content for msg in messages]) + + if text.strip(): + try: + # Use OpenAI or a simple logic to summarize + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "Summarize this conversation briefly."}, + {"role": "user", "content": text[:3000]} # avoid token limit + ] + ) + summary = response.choices[0].message.content.strip() + except Exception as e: + summary = "Summary generation failed." + + instance.summary = summary + instance.save(update_fields=["summary"]) \ No newline at end of file diff --git a/backend/chat/tasks.py b/backend/chat/tasks.py new file mode 100644 index 000000000..867979613 --- /dev/null +++ b/backend/chat/tasks.py @@ -0,0 +1,10 @@ +from celery import shared_task +from django.utils import timezone +from chat.models import Conversation +from datetime import timedelta + +@shared_task +def cleanup_old_conversations(): + cutoff = timezone.now() - timedelta(days=30) + deleted = Conversation.objects.filter(created_at_lt=cutoff, deleted_at_isnull=True).update(deleted_at=timezone.now()) + return f"{deleted} conversations soft-deleted." \ No newline at end of file diff --git a/backend/chat/urls.py b/backend/chat/urls.py index bd8ceadc0..e5e08c8e4 100644 --- a/backend/chat/urls.py +++ b/backend/chat/urls.py @@ -1,6 +1,11 @@ from django.urls import path from chat import views +# task-3 import +from .views import ConversationSummaryListView +from .views import FileUploadView +from .views import FileListView +# from .views import FileDeleteView urlpatterns = [ path("", views.chat_root_view, name="chat_root_view"), @@ -19,4 +24,14 @@ ), path("conversations//delete/", views.conversation_soft_delete, name="conversation_delete"), path("versions//add_message/", views.version_add_message, name="version_add_message"), + # task 3 + path('summary-conversations/', ConversationSummaryListView.as_view(), name='summary-conversations'), + path('upload/', FileUploadView.as_view(), name='file-upload'), + path('files/', FileListView.as_view(), name='file-list'), + # path('files//delete', FileDeleteView.as_view(), name='file-delete'), + # task-4 + path('delete//', views.delete_uploaded_file, name='delete-file'), + path('conversation//summary/', views.get_conversation_summary, name='conversation-summary'), + +path('conversation//', views.conversation_manage, name='conversation-manage'), ] diff --git a/backend/chat/views.py b/backend/chat/views.py index 0d18f7a69..c21b202e8 100644 --- a/backend/chat/views.py +++ b/backend/chat/views.py @@ -1,12 +1,55 @@ from django.contrib.auth.decorators import login_required from django.utils import timezone from rest_framework import status -from rest_framework.decorators import api_view -from rest_framework.response import Response +from rest_framework.decorators import api_view, permission_classes + + from chat.models import Conversation, Message, Version from chat.serializers import ConversationSerializer, MessageSerializer, TitleSerializer, VersionSerializer from chat.utils.branching import make_branched_conversation +# task-3 imports-1.0 Destroyapi view -3.4 delete file end point +from rest_framework.generics import ListAPIView +from rest_framework.filters import SearchFilter +from django_filters.rest_framework import DjangoFilterBackend +from chat.serializers import ConversationSummarySerializer + +# task-3.2file upload with duplicate check +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from .serializers import UploadedFileSerializer +# task-3.3 List Uploaded Files with Metadata +from .models import FileUpload + +class FileUploadView(APIView): + def post(self, request): + serializer = UploadedFileSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=201) + return Response(serializer.errors, status=400) + +class FileListView(ListAPIView): + queryset = FileUpload.objects.all().order_by('-uploaded_at') + serializer_class = UploadedFileSerializer + + + + + + +# task 3-1.0 +class ConversationSummaryListView(ListAPIView): + queryset = Conversation.objects.filter(deleted_at__isnull=True).order_by('-created_at') + serializer_class = ConversationSummarySerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['title', 'summary'] + + def get_queryset(self): + user = self.request.user + return Conversation.objects.filter(user=user, deleted_at__isnull=True).order_by('-created_at') +# end---------------------- @api_view(["GET"]) @@ -230,3 +273,100 @@ def version_add_message(request, pk): status=status.HTTP_201_CREATED, ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +# task-4 imports +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated + + +from .models import FileUpload +from .serializers import UploadedFileSerializer + +import logging +from .models import FileLog + +logger = logging.getLogger(__name__) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def upload_file_view(request): + if not request.user.is_staff: + return Response({'detail': 'Permission denied. Admins only.'}, status=403) + + if 'file' not in request.FILES: + return Response({'error': 'No file provided.'}, status=400) + + file_obj = request.FILES['file'] + uploaded_file =FileUpload.objects.create(user=request.user, file=file_obj) + + # Log to console + logger.info(f"{request.user.email} uploaded file: {file_obj.name}") + + # Log to DataBase + FileLog.objects.create(user=request.user, file_name=file_obj.name, action='UPLOAD') + + return Response({'message': 'File uploaded successfully.'}, status=201) + +@api_view(['DELETE']) +@permission_classes([IsAuthenticated]) +def delete_uploaded_file(request, file_id): + try: + uploaded_file = FileUpload.objects.get(id=file_id) + file_name = uploaded_file.file.name + uploaded_file.delete() + + # Create a FileLog entry + FileLog.objects.create( + user=request.user, + action='DELETE', + file_name=file_name + ) + + logger.info(f"{request.user.email} deleted file: {file_name}") + return Response({'message': 'File deleted successfully.'}, status=200) + except FileUpload.DoesNotExist: + return Response({'error': 'File not found.'}, status=404) + + +from django.core.cache import cache +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from chat.models import Conversation + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_conversation_summary(request, id): + cache_key = f"conversation_summary_{id}" + summary = cache.get(cache_key) + + if summary is None: + try: + conversation = Conversation.objects.get(id=id, user=request.user) + except Conversation.DoesNotExist: + return Response({"error": "Conversation not found"}, status=404) + + summary = conversation.summary or "No summary available" + cache.set(cache_key, summary, timeout=3600) # cache for 1 hour + + return Response({"id": id, "summary": summary}) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { 'class': 'logging.StreamHandler' }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} + + + + + + + + + diff --git a/backend/uploads/Dhanush_IT_Resume.pdf b/backend/uploads/Dhanush_IT_Resume.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_1.pdf b/backend/uploads/Dhanush_IT_Resume_1.pdf new file mode 100644 index 000000000..e27d127bc Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_1.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_1_tWv8tTc.pdf b/backend/uploads/Dhanush_IT_Resume_1_tWv8tTc.pdf new file mode 100644 index 000000000..e27d127bc Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_1_tWv8tTc.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_DMZ6647.pdf b/backend/uploads/Dhanush_IT_Resume_DMZ6647.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_DMZ6647.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_PNIV1vY.pdf b/backend/uploads/Dhanush_IT_Resume_PNIV1vY.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_PNIV1vY.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_UgtaNPe.pdf b/backend/uploads/Dhanush_IT_Resume_UgtaNPe.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_UgtaNPe.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_ZRPVr3V.pdf b/backend/uploads/Dhanush_IT_Resume_ZRPVr3V.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_ZRPVr3V.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_q5CkKp8.pdf b/backend/uploads/Dhanush_IT_Resume_q5CkKp8.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_q5CkKp8.pdf differ diff --git a/backend/uploads/Dhanush_IT_Resume_qxsgGx1.pdf b/backend/uploads/Dhanush_IT_Resume_qxsgGx1.pdf new file mode 100644 index 000000000..628b33bad Binary files /dev/null and b/backend/uploads/Dhanush_IT_Resume_qxsgGx1.pdf differ diff --git a/backend/uploads/IT_Resume.pdf b/backend/uploads/IT_Resume.pdf new file mode 100644 index 000000000..d14f87584 Binary files /dev/null and b/backend/uploads/IT_Resume.pdf differ