mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2024-12-25 18:22:23 +00: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:
parent
aac4fc59e6
commit
c45071c038
@ -43,4 +43,4 @@ class Command(BaseCommand):
|
|||||||
pool.parse_spreadsheet()
|
pool.parse_spreadsheet()
|
||||||
sleep(3) # Three calls = 3s sleep
|
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}
|
body = {"requests": format_requests}
|
||||||
worksheet.client.batch_update(spreadsheet.id, body)
|
worksheet.client.batch_update(spreadsheet.id, body)
|
||||||
|
|
||||||
def parse_tweaks_spreadskeets(self):
|
def parse_tweaks_spreadsheets(self):
|
||||||
if not self.pools.exists():
|
if not self.pools.exists():
|
||||||
# Draw has not been done yet
|
# Draw has not been done yet
|
||||||
return
|
return
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \
|
from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotificationsView, JoinTeamView, \
|
||||||
MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
|
MyParticipationDetailView, MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
|
||||||
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
|
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
|
||||||
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
|
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
|
||||||
ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
|
ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
|
||||||
@ -46,6 +46,8 @@ urlpatterns = [
|
|||||||
name="tournament_syntheses"),
|
name="tournament_syntheses"),
|
||||||
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
|
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
|
||||||
name="tournament_notation_sheets"),
|
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(),
|
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
|
||||||
name="tournament_publish_notes"),
|
name="tournament_publish_notes"),
|
||||||
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
|
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import asyncio
|
||||||
import csv
|
import csv
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from hashlib import sha1
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -9,6 +11,7 @@ from tempfile import mkdtemp
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
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.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.crypto import get_random_string
|
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.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 import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.views.generic.edit import FormMixin, ProcessFormView
|
from django.views.generic.edit import FormMixin, ProcessFormView
|
||||||
@ -1874,6 +1879,29 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
return response
|
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):
|
class PassageDetailView(LoginRequiredMixin, DetailView):
|
||||||
model = Passage
|
model = Passage
|
||||||
|
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null
|
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
|
# 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
|
# Clean temporary files
|
||||||
30 * * * * rm -rf /tmp/*
|
30 * * * * rm -rf /tmp/*
|
||||||
|
Loading…
Reference in New Issue
Block a user