From cabed1cc94ec1fec57f86b65abcefba91bc74624 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Tue, 6 May 2025 21:06:23 +0530 Subject: [PATCH] feat: [WIP] Automate GDrive Access for approved volunteers --- portal/settings.py | 6 ++ requirements-app.txt | 4 +- volunteer/admin.py | 6 ++ volunteer/apps.py | 3 + volunteer/gdrive_utils.py | 120 ++++++++++++++++++++++++++++++++++++++ volunteer/signals.py | 34 +++++++++++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 volunteer/gdrive_utils.py create mode 100644 volunteer/signals.py diff --git a/portal/settings.py b/portal/settings.py index 0484717..ae9f4d8 100644 --- a/portal/settings.py +++ b/portal/settings.py @@ -276,3 +276,9 @@ MEDIA_URL = "/media/" # URL to serve media files MEDIA_ROOT = os.path.join(BASE_DIR, "media") # Local filesystem path + +# Google Drive Settings +GOOGLE_SERVICE_ACCOUNT_FILE = os.path.join( + BASE_DIR, "krishnapachauri-208910-574aa1e639b3.json" +) +GOOGLE_DRIVE_FOLDER_ID = "18P_t0wpEXuMQyQkZJxccsacOh-eelLDk" diff --git a/requirements-app.txt b/requirements-app.txt index 0c49abe..1c7dc12 100644 --- a/requirements-app.txt +++ b/requirements-app.txt @@ -10,4 +10,6 @@ whitenoise==6.9.0 dj-database-url==2.3.0 boto3==1.38.5 django-storages==1.14.6 -pillow==11.2.1 \ No newline at end of file +pillow==11.2.1 +google-auth>=1.0.0 +google-api-python-client>=2.0.0 \ No newline at end of file diff --git a/volunteer/admin.py b/volunteer/admin.py index a715a24..d47a685 100644 --- a/volunteer/admin.py +++ b/volunteer/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin +from .constants import ApplicationStatus from .models import Role, Team, VolunteerProfile @@ -8,6 +9,11 @@ class VolunteerProfileAdmin(admin.ModelAdmin): search_fields = ("user__email", "user__first_name", "user__last_name") list_filter = ("timezone", "application_status") + def approve_volunteers(self, request, queryset): + for profile in queryset: + profile.application_status = ApplicationStatus.APPROVED + profile.save() + admin.site.register(Role) admin.site.register(Team) diff --git a/volunteer/apps.py b/volunteer/apps.py index 7df17b6..c7d824d 100644 --- a/volunteer/apps.py +++ b/volunteer/apps.py @@ -4,3 +4,6 @@ class VolunteerConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "volunteer" + + def ready(self): + import volunteer.signals # noqa: F401 diff --git a/volunteer/gdrive_utils.py b/volunteer/gdrive_utils.py new file mode 100644 index 0000000..32566fd --- /dev/null +++ b/volunteer/gdrive_utils.py @@ -0,0 +1,120 @@ +import logging + +from django.conf import settings +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +logger = logging.getLogger(__name__) + +SCOPES = ["https://www.googleapis.com/auth/drive"] +SERVICE_ACCOUNT_FILE = settings.GOOGLE_SERVICE_ACCOUNT_FILE +FOLDER_ID = settings.GOOGLE_DRIVE_FOLDER_ID + + +def validate_folder_id(): + service = get_gdrive_service() + try: + service.files().get(fileId=FOLDER_ID, fields="id,name").execute() + return True + except HttpError as e: + if e.resp.status == 404: + logger.error( + f"Folder ID {FOLDER_ID} not found. Please check the folder ID in settings." + ) + else: + logger.error(f"Error validating folder ID: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error validating folder ID: {str(e)}") + return False + + +def get_gdrive_service(): + credentials = service_account.Credentials.from_service_account_file( + SERVICE_ACCOUNT_FILE, scopes=SCOPES + ) + return build("drive", "v3", credentials=credentials) + + +def add_to_gdrive(email): + service = get_gdrive_service() + try: + if not validate_folder_id(): + logger.error(f"Cannot add {email} to GDrive: Invalid folder ID") + return + + permissions = ( + service.permissions() + .list(fileId=FOLDER_ID, fields="permissions(id, emailAddress)") + .execute() + .get("permissions", []) + ) + + existing = [p for p in permissions if p.get("emailAddress") == email] + + if existing: + logger.info(f"Email {email} already has access.") + return + + permission = {"type": "user", "role": "writer", "emailAddress": email} + + result = ( + service.permissions() + .create( + fileId=FOLDER_ID, + body=permission, + fields="id", + sendNotificationEmail=False, + ) + .execute() + ) + + logger.info(f"Added {email} to GDrive with permission ID: {result.get('id')}") + + except HttpError as e: + if e.resp.status == 404: + logger.error( + f"Google API error adding {email}: Folder not found. Check GOOGLE_DRIVE_FOLDER_ID in settings." + ) + else: + logger.error(f"Google API error adding {email}: {str(e)}") + except Exception as e: + logger.error(f"Error adding {email} to GDrive: {str(e)}") + + +def remove_from_gdrive(email): + service = get_gdrive_service() + try: + if not validate_folder_id(): + logger.error(f"Cannot remove {email} from GDrive: Invalid folder ID") + return + + permissions = ( + service.permissions() + .list(fileId=FOLDER_ID, fields="permissions(id, emailAddress)") + .execute() + .get("permissions", []) + ) + + target_permissions = [p for p in permissions if p.get("emailAddress") == email] + + if not target_permissions: + logger.info(f"No permissions found for {email} to remove") + return + + for p in target_permissions: + service.permissions().delete( + fileId=FOLDER_ID, permissionId=p["id"] + ).execute() + logger.info(f"Removed {email} from GDrive (permission ID: {p['id']})") + + except HttpError as e: + if e.resp.status == 404: + logger.error( + f"Google API error removing {email}: Folder not found. Check GOOGLE_DRIVE_FOLDER_ID in settings." + ) + else: + logger.error(f"Google API error removing {email}: {str(e)}") + except Exception as e: + logger.error(f"Error removing {email} from GDrive: {str(e)}") diff --git a/volunteer/signals.py b/volunteer/signals.py new file mode 100644 index 0000000..23f2f0c --- /dev/null +++ b/volunteer/signals.py @@ -0,0 +1,34 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from .constants import ApplicationStatus +from .gdrive_utils import add_to_gdrive, remove_from_gdrive +from .models import VolunteerProfile + + +@receiver(pre_save, sender=VolunteerProfile) +def track_approval_status(sender, instance, **kwargs): + """Capture previous status before save""" + try: + original = VolunteerProfile.objects.get(pk=instance.pk) + instance._original_application_status = original.application_status + except VolunteerProfile.DoesNotExist: + instance._original_application_status = None + + +@receiver(post_save, sender=VolunteerProfile) +def handle_approval_status(sender, instance, **kwargs): + """Handle GDrive access based on status changes""" + original_status = instance._original_application_status + new_status = instance.application_status + + email = instance.user.email + + if new_status == ApplicationStatus.APPROVED: + # New approval or re-approval + if original_status != ApplicationStatus.APPROVED and email: + add_to_gdrive(email) + else: + # Revoked approval + if original_status == ApplicationStatus.APPROVED and email: + remove_from_gdrive(email)