Compare commits

...

5 Commits

Author SHA1 Message Date
Emmy D'Anello 758f714096
Add supportAllDrives=true parameter to GDrive notifications
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-04-06 22:18:22 +02:00
Emmy D'Anello 40d24740ed
Fix import orders
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-04-06 22:05:48 +02:00
Emmy D'Anello b7344566ef
Only accept GDrive notifications if the content was updated
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-04-06 22:04:55 +02:00
Emmy D'Anello 0f5d0c8b40
Add try/catch in Google Sheets scripts
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-04-06 21:57:34 +02:00
Emmy D'Anello c45071c038
Add notifications from Google Drive to automatically get updates from Google Sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-04-06 21:55:46 +02:00
6 changed files with 115 additions and 7 deletions

View File

@ -40,7 +40,17 @@ class Command(BaseCommand):
for pool in pools.all():
if options['verbosity'] >= 1:
self.stdout.write(f"Parsing notation sheet for pool {pool.short_name} for {tournament}")
pool.parse_spreadsheet()
sleep(3) # Three calls = 3s sleep
try:
pool.parse_spreadsheet()
except Exception as e:
if options['verbosity'] >= 1:
self.stderr.write(
self.style.ERROR(f"Error while parsing pool {pool.short_name} for {tournament.name}: {e}"))
finally:
sleep(3) # Three calls = 3s sleep
tournament.parse_tweaks_spreadskeets()
try:
tournament.parse_tweaks_spreadsheets()
except Exception as e:
if options['verbosity'] >= 1:
self.stderr.write(self.style.ERROR(f"Error while parsing tweaks for {tournament.name}: {e}"))

View File

@ -0,0 +1,61 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import sha1
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
import gspread
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?supportsAllDrives=true"
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())),
}
try:
http_client.request(method="POST", endpoint=url, json=body).raise_for_status()
except Exception as e:
self.stderr.write(self.style.ERROR(f"Error while renewing notifications for {tournament.name}: {e}"))

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,10 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
from concurrent.futures import ThreadPoolExecutor
import csv
from hashlib import sha1
from io import BytesIO
import os
import subprocess
@ -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,33 @@ 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']}")
if request.headers['X-Goog-Resource-State'] != 'update' \
or 'content' not in request.headers['X-Goog-Changed'].split(','):
return HttpResponse(status=204)
# 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/*