# Copyright (C) 2017 Alban Gruin # # celcatsanitizer is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # celcatsanitizer is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . import datetime import re from bs4 import BeautifulSoup from django.utils import timezone from edt.models import Group, Room, Course from edt.utils import get_week import requests import edt def add_time(date, time): ptime = datetime.datetime.strptime(time, "%H:%M") delta = datetime.timedelta(hours=ptime.hour, minutes=ptime.minute) return date + delta def delete_courses_in_week(timetable, year, week, today): start, end = get_week(year, week) Course.objects.filter(begin__gte=max(start, today), begin__lt=end, timetable=timetable).delete() def get_from_db_or_create(cls, **kwargs): obj = cls.objects.all().filter(**kwargs) obj = obj.first() if obj is None: obj = cls(**kwargs) obj.save() return obj def get_event(timetable, event, event_week, today): """Renvoie une classe Course à partir d’un événement récupéré par BS4""" # On récupère la date de l’évènement à partir de la semaine # et de la semaine référencée, puis l’heure de début et de fin date = event_week + datetime.timedelta(int(event.day.text)) begin = add_time(date, event.starttime.text) end = add_time(date, event.endtime.text) # On ne traite pas le cours si il commence après le moment du traitement if today is not None and begin < today: return # Création de l’objet cours course = Course.objects.create(timetable=timetable, begin=begin, end=end) # On récupère les groupes concernés par les cours groups = [get_from_db_or_create(Group, timetable=timetable, celcat_name=item.text) for item in event.resources.group.find_all("item")] course.groups.add(*groups) # On récupère le champ « remarque » if event.notes is not None: course.notes = event.notes.text # On récupère le champ « nom » if event.resources.module is not None: course.name = event.resources.module.item.text elif event.category is not None: # Il est possible qu’un cours n’ait pas de nom. Oui oui. # Qui sont les concepteurs de ce système ? Quels sont leurs # réseaux ? # Bref, dans ce cas, si le cours a un type, il devient son nom. course.type = event.category.text # Si il n’a pas de type (mais je ne pense pas que ça soit possible…), # il obtiendra une valeur par défaut définie à l’avance. # Récupération du type de cours if event.category is not None: course.type = event.category.text # Si un cours a une salle attribuée (oui, il est possible qu’il n’y # en ait pas… qui sont ils, leurs réseaux, tout ça…), on les insère # dans la base de données, et on les ajoute dans l’objet cours if event.resources.room is not None: rooms = [get_from_db_or_create(Room, name=item.text) for item in event.resources.room.find_all("item")] course.rooms.add(*rooms) return course def get_events(timetable, soup, weeks_in_soup, today, year=None, week=None): """Récupère tous les cours disponibles dans l’emploi du temps Celcat. Le traîtement se limitera à la semaine indiquée si il y en a une.""" for event in soup.find_all("event"): event_week = weeks_in_soup[event.rawweeks.text] event_week_num = event_week.isocalendar()[1] # Numéro de semaine # On passe le traitement si la semaine de l’événement ne correspond pas # à la semaine passée, ou qu’il ne contient pas de groupe ou n’a pas de # date de début ou de fin. if (event_week_num == week and event_week.year == year or \ year is None or week is None) and \ event.resources.group is not None and \ event.starttime is not None and event.endtime is not None: course = get_event(timetable, event, event_week, today) # On renvoie le cours si il n’est pas nul if course is not None: yield course def get_update_date(soup): # Explication de la regex # # (\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+) # (\d+) au moins un nombre # / un slash # (\d+) au moins un nombre # / un slash # (\d+) au moins un nombre # \s+ au moins un espace # (\d+) au moins un nombre # : un deux-points # (\d+) au moins un nombre # : un deux-points # (\d+) au moins un nombre datetime_regex = re.compile(r"(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+)") search = datetime_regex.search(soup.footer.text) if search is None: return None day, month, year, hour, minute, second = [int(v) for v in search.groups()] date = datetime.datetime(year, month, day, hour, minute, second) return timezone.make_aware(date) def get_weeks(soup): # Les semaines sont référencées de manière assez… exotique # En gros, il y a une liste d’éléments span qui contiennent une sorte d’ID # de la semaine, formaté de la manière suivante : # NNNNNNNNNNNNNNNNNNNYNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN # Tous sont de la même longueur, contiennent 51 N et un seul Y. # Allez savoir pourquoi. Il se trouve dans la balise « alleventweeks ». # Un paramètre du span (« date ») représente la date de début. # Un cours contient donc un ID de semaine, puis le nombre de jours après le # début de cette semaine. weeks = {} for span in soup.find_all("span"): # Liste de toutes les semaines définies # On parse la date et on la fait correspondre à l’ID weeks[span.alleventweeks.text] = timezone.make_aware( datetime.datetime.strptime(span["date"], "%d/%m/%Y")) return weeks def get_xml(url): user_agent = "celcatsanitizer/" + edt.VERSION req = requests.get(url, headers={"User-Agent": user_agent}) req.encoding = "utf8" soup = BeautifulSoup(req.content, "html.parser") return soup