mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-11-04 01:32:05 +01:00 
			
		
		
		
	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:
		@@ -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/*
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user