diff options
| author | Alban Gruin | 2018-08-25 22:54:33 +0200 | 
|---|---|---|
| committer | Alban Gruin | 2018-09-03 19:18:58 +0200 | 
| commit | e7f2ccdd870f998b9199b85bf2c486f8d1d0cceb (patch) | |
| tree | b33f466fb0264a47964d6cfe85a83c17227ccf7e /management/parsers | |
| parent | 5488a93bf2e04d2f19e287186011dcbb436a238b (diff) | |
management: création d’un sous-module parser
Signed-off-by: Alban Gruin <alban at pa1ch dot fr>
Diffstat (limited to 'management/parsers')
| -rw-r--r-- | management/parsers/ups2017.py | 164 | 
1 files changed, 164 insertions, 0 deletions
diff --git a/management/parsers/ups2017.py b/management/parsers/ups2017.py new file mode 100644 index 0000000..94c1918 --- /dev/null +++ b/management/parsers/ups2017.py @@ -0,0 +1,164 @@ +#    Copyright (C) 2017-2018  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 <http://www.gnu.org/licenses/>. + +import datetime +import re + +from bs4 import BeautifulSoup +from django.utils import timezone + +from ...models import Course, Group, Room +from ...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(source, year, week, today): +    start, end = get_week(year, week) +    Course.objects.filter(begin__gte=max(start, today), begin__lt=end, +                          source=source).delete() + + +def get_event(source, 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(source=source, begin=begin, end=end) + +    # On récupère les groupes concernés par les cours +    groups = [Group.objects.get_or_create(source=source, +                                          celcat_name=item.text)[0] +              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 = "\n".join(event.notes.find_all(text=True)) + +    # 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 = [Room.objects.get_or_create(name=item.text)[0] +                 for item in event.resources.room.find_all("item")] +        course.rooms.add(*rooms) + +    return course + + +def get_events(source, 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(source, 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  | 
