Add notifications from Google Drive to automatically get updates from Google Sheets

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2024-04-06 21:55:46 +02:00
parent aac4fc59e6
commit c45071c038
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
6 changed files with 97 additions and 6 deletions

View File

@ -43,4 +43,4 @@ class Command(BaseCommand):
pool.parse_spreadsheet()
sleep(3) # Three calls = 3s sleep
tournament.parse_tweaks_spreadskeets()
tournament.parse_tweaks_spreadsheets()

View File

@ -0,0 +1,58 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import sha1
import gspread
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.management import BaseCommand
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import localtime
from ...models import Tournament
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
)
def handle(self, *args, **options):
tournaments = Tournament.objects.all() if not options['tournament'] \
else Tournament.objects.filter(name=options['tournament']).all()
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
http_client = gc.http_client
http_client.login()
site = Site.objects.get(pk=settings.SITE_ID)
now = localtime(timezone.now())
tomorrow = now + timezone.timedelta(days=1)
tomorrow -= timezone.timedelta(hours=now.hour, minutes=now.minute, seconds=now.second,
microseconds=now.microsecond)
for tournament in tournaments:
if options['verbosity'] >= 1:
self.stdout.write(f"Renewing Google Drive notifications for {tournament}")
if not tournament.notes_sheet_id:
if options['verbosity'] >= 1:
self.stdout.write(
self.style.WARNING(f"No spreadsheet found for {tournament}. Please create it first"))
continue
channel_id = sha1(f"{tournament.name}-{timezone.now().date()}-{site.domain}".encode()).hexdigest()
url = f"https://www.googleapis.com/drive/v3/files/{tournament.notes_sheet_id}/watch"
notif_path = reverse('participation:tournament_gsheet_notifications', args=[tournament.pk])
notif_url = f"https://{site.domain}{notif_path}"
body = {
"id": channel_id,
"type": "web_hook",
"address": notif_url,
"expiration": str(int(1000 * tomorrow.timestamp())),
}
http_client.request(method="POST", endpoint=url, json=body).raise_for_status()

View File

@ -606,7 +606,7 @@ class Tournament(models.Model):
body = {"requests": format_requests}
worksheet.client.batch_update(spreadsheet.id, body)
def parse_tweaks_spreadskeets(self):
def parse_tweaks_spreadsheets(self):
if not self.pools.exists():
# Draw has not been done yet
return

View File

@ -4,8 +4,8 @@
from django.urls import path
from django.views.generic import TemplateView
from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \
MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotificationsView, JoinTeamView, \
MyParticipationDetailView, MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
@ -46,6 +46,8 @@ urlpatterns = [
name="tournament_syntheses"),
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
name="tournament_notation_sheets"),
path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(),
name="tournament_gsheet_notifications"),
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
name="tournament_publish_notes"),
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),

View File

@ -1,7 +1,9 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
import csv
from concurrent.futures import ThreadPoolExecutor
from hashlib import sha1
from io import BytesIO
import os
import subprocess
@ -9,6 +11,7 @@ from tempfile import mkdtemp
from typing import Any, Dict
from zipfile import ZipFile
from asgiref.sync import sync_to_async
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
@ -23,7 +26,9 @@ from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin, ProcessFormView
@ -1874,6 +1879,29 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
return response
@method_decorator(csrf_exempt, name='dispatch')
class GSheetNotificationsView(View):
async def post(self, request, *args, **kwargs):
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
return HttpResponse(status=404)
tournament = await Tournament.objects.prefetch_related('participations', 'pools').aget(pk=kwargs['pk'])
request.site.domain = "test.ynerant.fr"
expected_channel_id = sha1(f"{tournament.name}{timezone.now().date()}{request.site.domain}".encode()) \
.hexdigest()
if request.headers['X-Goog-Channel-ID'] != expected_channel_id:
raise ValueError(f"Invalid channel ID: {request.headers['X-Goog-Channel-ID']}")
# Run the parsing in dedicated executors since it takes time
executor = ThreadPoolExecutor()
async for pool in tournament.pools.prefetch_related('participations', 'passages__notes', 'juries').all():
asyncio.get_event_loop().run_in_executor(executor, pool.parse_spreadsheet)
asyncio.get_event_loop().run_in_executor(executor, tournament.parse_tweaks_spreadsheets)
return HttpResponse(status=204)
class PassageDetailView(LoginRequiredMixin, DetailView):
model = Passage

View File

@ -16,7 +16,10 @@
30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null
# Check notation sheets every 15 minutes from 08:00 to 23:00 on fridays to mondays in april and may
*/15 8-23 * 4-5 5,6,7,1 cd /code && python manage.py parse_notation_sheets -v 0
# */15 8-23 * 4-5 5,6,7,1 cd /code && python manage.py parse_notation_sheets -v 0
# Update Google Drive notifications daily
0 0 * * * cd /code && python manage.py renew_gdrive_notifications &> /dev/null
# Clean temporary files
30 * * * * rm -rf /tmp/*