From d935c1b7d2b7637195e0407d26675408777cb4ed Mon Sep 17 00:00:00 2001 From: Yoan Le Clanche Date: Wed, 18 Jan 2023 12:02:25 +0100 Subject: [PATCH] Cache handling and notify live conferences --- app/settings.py | 4 ++ .../commands/teleforma-build-cache.py | 52 +++++++++++++++++++ .../0022_conference_notified_live.py | 18 +++++++ teleforma/models/core.py | 12 ++++- teleforma/models/crfpa.py | 19 ++++++- teleforma/urls.py | 6 ++- teleforma/views/core.py | 38 ++++++++++++-- 7 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 teleforma/management/commands/teleforma-build-cache.py create mode 100644 teleforma/migrations/0022_conference_notified_live.py diff --git a/app/settings.py b/app/settings.py index 95526b1b..402b58d8 100644 --- a/app/settings.py +++ b/app/settings.py @@ -35,6 +35,7 @@ ALLOWED_HOSTS = ['localhost', 'crfpa.dockdev.pilotsystems.net', 'e-learning.crfpa.pre-barreau.com', 'prod.docker.e-learning.crfpa.pre-barreau.parisson.com', 'recovery.docker.e-learning.crfpa.pre-barreau.parisson.com', + 'channels' ] ASGI_APPLICATION = "teleforma.ws.routing.application" @@ -58,6 +59,9 @@ CHANNEL_LAYERS = { }, } +# channel access point from django +CHANNEL_URL = "http://channels:8000" + DATABASES = { 'default': { # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. diff --git a/teleforma/management/commands/teleforma-build-cache.py b/teleforma/management/commands/teleforma-build-cache.py new file mode 100644 index 00000000..dd15e8d4 --- /dev/null +++ b/teleforma/management/commands/teleforma-build-cache.py @@ -0,0 +1,52 @@ + +from django.core.management.base import BaseCommand, CommandError +from django.template.defaultfilters import slugify +from django.conf import settings +from teleforma.models.crfpa import Student +import logging + +from teleforma.views.core import get_courses + + +class Logger: + """A logging object""" + + def __init__(self, file): + self.logger = logging.getLogger('myapp') + if file: + self.hdlr = logging.FileHandler(file) + else: + self.hdlr = None + self.formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + if self.hdlr: + self.hdlr.setFormatter(self.formatter) + self.logger.addHandler(self.hdlr) + self.logger.addHandler(logging.StreamHandler()) + self.logger.setLevel(logging.INFO) + +class Command(BaseCommand): + help = "Build get_courses cache" + + def add_arguments(self, parser): + parser.add_argument('--logfile', type=str, required=False, + help='log file to use') + + def handle(self, *args, **options): + logpath = options['logfile'] + logger = Logger(logpath) + + total = Student.objects.filter(user__is_active=True).count() + logger.logger.info(f'Found {total} active students...') + for i, student in enumerate(Student.objects.filter(user__is_active=True, platform_only=True)): + if i % 100 == 0: + logger.logger.info(f"build cache : {int(i * 100 / total)}%") + periods = [training.period for training in student.trainings.all()] + for period in periods: + for child in period.children.all(): + periods.append(child) + for period in periods + [None]: + # for date_order in (True, False): + # for num_order in (True, False): + # for num_courses in (True, False): + # logger.logger.info(f"build cache for get_courses-{student.user.id}-{date_order}-{num_order}-{num_courses}-{period and period.id or None}") + get_courses(student.user, period=period) diff --git a/teleforma/migrations/0022_conference_notified_live.py b/teleforma/migrations/0022_conference_notified_live.py new file mode 100644 index 00000000..0431fe54 --- /dev/null +++ b/teleforma/migrations/0022_conference_notified_live.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2023-01-17 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teleforma', '0021_conferencepublication'), + ] + + operations = [ + migrations.AddField( + model_name='conference', + name='notified_live', + field=models.BooleanField(default=False, verbose_name='Notification live'), + ), + ] diff --git a/teleforma/models/core.py b/teleforma/models/core.py index 125e2018..c9b9fbba 100755 --- a/teleforma/models/core.py +++ b/teleforma/models/core.py @@ -39,6 +39,7 @@ import mimetypes import os import string import random +import requests from teleforma.utils import guess_mimetypes import django.db.models as models @@ -48,7 +49,7 @@ from django.core.paginator import InvalidPage from django.db import models from django.forms.fields import FileField from django.template.defaultfilters import slugify -from django.urls import reverse_lazy +from django.urls import reverse_lazy, reverse from django.utils.translation import ugettext_lazy as _ # from quiz.models import Quiz from sorl.thumbnail import default as sorl_default @@ -406,6 +407,7 @@ class Conference(models.Model): web_class_group = models.ForeignKey('WebClassGroup', related_name='conferences', verbose_name=_('web class group'), blank=True, null=True, on_delete=models.SET_NULL) notified = models.BooleanField(_('notified'), default=False) + notified_live = models.BooleanField("Notifié live", default=False) @property def description(self): @@ -452,6 +454,14 @@ class Conference(models.Model): if not self.public_id: self.public_id = get_random_hash() self.course.save() + + if self.streaming and not self.notified_live: + # Notify live conferences by sending a signal to websocket. + # This signal will be catched by the channel instance to notify students + from teleforma.models.notification import notify + requests.post(f"{settings.CHANNEL_URL}{reverse('teleforma-live-conference-notify')}", {'id': self.id}) + self.notified_live = True + super(Conference, self).save(*args, **kwargs) def to_dict(self): diff --git a/teleforma/models/crfpa.py b/teleforma/models/crfpa.py index e3e5a4f8..bbf58fae 100755 --- a/teleforma/models/crfpa.py +++ b/teleforma/models/crfpa.py @@ -44,6 +44,7 @@ from django.db.models import signals from django.urls.base import reverse_lazy from django.utils.translation import ugettext_lazy as _ from tinymce.models import HTMLField +from django.core.cache import cache from ..models.core import (Course, Media, MetaCore, payment_choices, payment_schedule_choices) @@ -401,6 +402,7 @@ class Student(models.Model): def expiration_date(self): """ closing date of student period """ return self.period.date_close_accounts + class Meta(MetaCore): db_table = app_label + '_' + 'student' @@ -419,11 +421,26 @@ def update_balance_signal(sender, instance, *args, **kwargs): def create_payment_objects(sender, instance, *args, **kwargs): transaction.on_commit(instance.create_payment_objects) + +def purge_courses_cache(sender, instance, *args, **kwargs): + """ purge get_courses cache """ + periods = [training.period for training in instance.trainings.all()] + for period in periods: + for child in period.children.all(): + periods.append(child) + for period in periods + [None]: + for date_order in (True, False): + for num_order in (True, False): + for num_courses in (True, False): + cache_key = f"get_courses-{instance.user.id}-{date_order}-{num_order}-{num_courses}-{period and period.id or None}" + print(f"purging {cache_key}") + cache.delete(cache_key) + signals.post_save.connect(update_balance_signal) signals.post_save.connect(create_payment_objects, sender=Student) +signals.post_save.connect(purge_courses_cache, sender=Student) signals.post_delete.connect(update_balance_signal) - class Profile(models.Model): "User profile extension" diff --git a/teleforma/urls.py b/teleforma/urls.py index dfc2575d..9fe6dbf5 100644 --- a/teleforma/urls.py +++ b/teleforma/urls.py @@ -54,7 +54,7 @@ from teleforma.views.home import HomeView from .views.appointment import Appointments, cancel_appointment from .views.core import (ChatMessageView, ConferenceListView, ConferenceView, CourseListView, CoursePendingListView, CourseView, DocumentView, - HelpView, HomeRedirectView, MediaTranscodedView, + HelpView, HomeRedirectView, LiveConferenceNotify, MediaTranscodedView, MediaView, MediaViewEmbed, NotificationView) from .views.crfpa import (AnnalsCourseView, AnnalsIEJView, AnnalsView, CorrectorAddView, CorrectorCompleteView, @@ -272,4 +272,8 @@ urlpatterns = [ # notification path('notification', NotificationView.as_view(), name='teleforma-notification'), + + # must be called on channels instance + path('live_conference_notify', + LiveConferenceNotify.as_view(), name='teleforma-live-conference-notify') ] diff --git a/teleforma/views/core.py b/teleforma/views/core.py index 4b40403e..a718aa68 100644 --- a/teleforma/views/core.py +++ b/teleforma/views/core.py @@ -51,7 +51,7 @@ from django.contrib.sites.shortcuts import get_current_site from django.template import loader from django.urls import reverse from django.utils.decorators import method_decorator -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext, ugettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.views.generic.base import TemplateResponseMixin, TemplateView, View from django.views.generic.detail import DetailView @@ -67,8 +67,8 @@ from rest_framework.response import Response # # Authors: Guillaume Pellerin from rest_framework.views import APIView -from teleforma.models.crfpa import Home, Training -from teleforma.models.notification import Notification +from teleforma.models.crfpa import Home, Student, Training +from teleforma.models.notification import Notification, notify from teleforma.utils import guess_mimetypes from ..decorators import access_required @@ -82,6 +82,8 @@ from ..models.core import (Conference, ConferencePublication, Course, CourseType from ..webclass.models import Webclass, WebclassRecord from .pages import get_page_content +import logging +logger = logging.getLogger('teleforma') # def render(request, template, data=None, mimetype=None): # return django_render(template, data, context_instance=RequestContext(request), # mimetype=mimetype) @@ -968,6 +970,36 @@ class NotificationView(APIView): return Response({'status': 'ok'}) +class LiveConferenceNotify(APIView): + def post(self, request): + """ + notify users a new live conference is starting + """ + conference_id = request.data.get('id') + if not conference_id: + raise Exception('No conference id in request') + conference = Conference.objects.get(pk=int(conference_id)) + students = Student.objects.filter(period=conference.period, platform_only=True) + text = f"""Une conférence live "{conference.course.title}" commence""" + url = reverse('teleforma-conference-detail', kwargs={'period_id': conference.period.id, 'pk': conference.id}) + for student in students: + try: + if student.user: + courses = get_courses(student.user, period=conference.period) + for course in courses: + if conference.course == course['course'] and \ + conference.course_type in course['types']: + notify(student.user, text, url) + logger.info("Student notified: " + student.user.username) + except Exception as e: + logger.warning("Student NOT notified: " + str(student.id)) + logger.warning(e) + return Response({'status': 'ok'}) + + + + + # class ConferenceRecordView(FormView): # "Conference record form : TeleCaster module required" -- 2.39.5