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:
parent
aac4fc59e6
commit
c45071c038
|
@ -43,4 +43,4 @@ class Command(BaseCommand):
|
|||
pool.parse_spreadsheet()
|
||||
sleep(3) # Three calls = 3s sleep
|
||||
|
||||
tournament.parse_tweaks_spreadskeets()
|
||||
tournament.parse_tweaks_spreadsheets()
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/*
|
||||
|
|
Loading…
Reference in New Issue