Skip to content

feat: [WIP] Automate GDrive Access for approved volunteers #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions portal/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,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"
Copy link
Member

Choose a reason for hiding this comment

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

use env var instead of hardcoding

)
GOOGLE_DRIVE_FOLDER_ID = "18P_t0wpEXuMQyQkZJxccsacOh-eelLDk"
Copy link
Member

Choose a reason for hiding this comment

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

Use env var instead of hardcoding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I’ve make it a test version to verify the logic is correct or not, will fix it once we agree that this is what we gonna do.

4 changes: 3 additions & 1 deletion requirements-app.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ whitenoise==6.9.0
dj-database-url==2.3.0
boto3==1.38.5
django-storages==1.14.6
django-widget-tweaks==1.5.0
pillow==11.2.1
google-auth>=1.0.0
google-api-python-client>=2.0.0
django-widget-tweaks==1.5.0
6 changes: 6 additions & 0 deletions volunteer/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib import admin

from .constants import ApplicationStatus
from .models import Role, Team, VolunteerProfile


Expand All @@ -8,6 +9,11 @@ class VolunteerProfileAdmin(admin.ModelAdmin):
search_fields = ("user__email", "user__first_name", "user__last_name")
list_filter = ("region", "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)
Expand Down
3 changes: 3 additions & 0 deletions volunteer/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class VolunteerConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "volunteer"

def ready(self):
import volunteer.signals # noqa: F401
120 changes: 120 additions & 0 deletions volunteer/gdrive_utils.py
Original file line number Diff line number Diff line change
@@ -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)}")
34 changes: 34 additions & 0 deletions volunteer/signals.py
Original file line number Diff line number Diff line change
@@ -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)
Loading