diff options
Diffstat (limited to 'management')
| -rw-r--r-- | management/commands/_private.py | 150 | ||||
| -rw-r--r-- | management/commands/cleancourses.py | 4 | ||||
| -rw-r--r-- | management/commands/timetables.py | 80 | 
3 files changed, 128 insertions, 106 deletions
| diff --git a/management/commands/_private.py b/management/commands/_private.py index c140f51..8f195a1 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -13,33 +13,20 @@  #    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 edt.models import Group, Room, Course  from edt.utils import get_week -import datetime -import re -  import requests - -class Week: -    def __init__(self, number, start): -        self.number = number -        self.start = timezone.make_aware( -            datetime.datetime.strptime(start, "%d/%m/%Y")) - -    def get_day(self, id): -        return self.start + datetime.timedelta(id) - -    @property -    def year(self): -        return self.start.year -  def add_time(date, time): -    delta = datetime.timedelta(hours=time.hour, minutes=time.minute) +    ptime = datetime.datetime.strptime(time, "%H:%M") +    delta = datetime.timedelta(hours=ptime.hour, minutes=ptime.minute)      return date + delta  def consolidate_group(group): @@ -75,7 +62,7 @@ def consolidate_group(group):  def consolidate_groups(groups):      for group in groups: -        if group.parent == None: +        if group.parent is None:              consolidate_group(group)  def delete_courses_in_week(timetable, year, week): @@ -93,50 +80,69 @@ def get_from_db_or_create(cls, **kwargs):      return obj -def get_events(timetable, year, week, soup, weeks_in_soup): +def get_event(timetable, event, event_week): +    """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) + +    # 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, on les +    # « consolide », puis on les insère dans l’objet cours. +    groups = [get_from_db_or_create(Group, timetable=timetable, +                                    celcat_name=item.text) +              for item in event.resources.group.find_all("item")] +    consolidate_groups(groups) +    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 nom du cours +    if event.resources.module is not None: +        course.name = event.resources.module.item.text +    else: +        # 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, on déplace le champ « remarque » de +        # l’objet dans le champ « nom ». +        course.name, course.notes = course.notes, None + +    # 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, 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"): -        title = None -        type_ = None -        groups = None -        rooms = None -        notes = None - -        if weeks_in_soup[event.rawweeks.text].number == week and \ -           weeks_in_soup[event.rawweeks.text].year == year and \ +        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: -            date = weeks_in_soup[event.rawweeks.text].get_day(int( -                event.day.text)) - -            begin = add_time(date, datetime.datetime.strptime( -                event.starttime.text, "%H:%M")) -            end = add_time(date, datetime.datetime.strptime( -                event.endtime.text, "%H:%M")) - -            groups = [get_from_db_or_create(Group, timetable=timetable, -                                            celcat_name=item.text) -                      for item in event.resources.group.find_all("item")] -            consolidate_groups(groups) - -            if event.notes is not None: -                notes = event.notes.text - -            if event.resources.module is not None: -                title = event.resources.module.item.text -            elif notes is not None: -                title = notes -                notes = None -            else: -                title = "Aucune information" - -            if event.category is not None: -                type_ = event.category.text - -            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")] - -            yield title, type_, groups, rooms, notes, begin, end +            yield get_event(timetable, event, event_week)  def get_update_date(soup):      # Explication de la regex @@ -153,7 +159,7 @@ def get_update_date(soup):      #                           (\d+)       au moins un nombre      #                                :      un deux-points      #                                 (\d+) au moins un nombre -    datetime_regex = re.compile("(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+)") +    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 @@ -163,16 +169,26 @@ def get_update_date(soup):      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"): -        weeks[span.alleventweeks.text] = Week(int(span.title.text), -                                              span["date"]) +    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): -    r = requests.get(url) -    r.encoding = "utf8" +    req = requests.get(url) +    req.encoding = "utf8" -    soup = BeautifulSoup(r.content, "html.parser") +    soup = BeautifulSoup(req.content, "html.parser")      return soup diff --git a/management/commands/cleancourses.py b/management/commands/cleancourses.py index ca2ef94..f6041ef 100644 --- a/management/commands/cleancourses.py +++ b/management/commands/cleancourses.py @@ -15,7 +15,7 @@  from django.core.management.base import BaseCommand  from django.db import transaction -from edt.models import Course, Group, LastUpdate +from edt.models import Course, Group  class Command(BaseCommand): @@ -29,10 +29,8 @@ class Command(BaseCommand):              if options["timetable"] is None:                  Course.objects.all().delete()                  Group.objects.all().delete() -                LastUpdate.objects.all().delete()              else:                  Course.objects.filter(timetable__id__in=options["timetable"]).delete()                  Group.objects.filter(timetable__id__in=options["timetable"]).delete() -                LastUpdate.objects.filter(timetable__id__in=options["timetable"]).delete()          self.stdout.write(self.style.SUCCESS("Done.")) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index c82b0e4..76f0a7c 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -13,59 +13,63 @@  #    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 +  from django.core.management.base import BaseCommand  from django.db import transaction +from django.db.models import Min  from django.utils import timezone -from edt.models import Timetable, LastUpdate, Course +from edt.models import Course, Timetable +from edt.utils import get_week  from ._private import delete_courses_in_week, get_events, get_update_date, get_weeks, get_xml -import datetime -  @transaction.atomic -def process_timetable_week(timetable, year, week, soup, weeks_in_soup): -    last_update_date = None +def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, week=None): +    criteria = {} +    if year is not None and week is not None: +        begin, end = get_week(year, week) +        criteria["begin__gte"] = begin +        criteria["begin__lt"] = end + +    last_update_date = Course.objects.filter(timetable=timetable, **criteria) \ +                                     .aggregate(Min("last_update")) \ +                                     ["last_update__min"]      new_update_date = get_update_date(soup) -    try: -        last_update = LastUpdate.objects.get(timetable=timetable, year=year, week=week) -        last_update_date = last_update.updated_at -    except: -        last_update = LastUpdate(timetable=timetable, year=year, week=week) -    if last_update_date is not None and new_update_date is not None and \ +    if not force and last_update_date is not None and new_update_date is not None and \         last_update_date >= new_update_date:          return -    delete_courses_in_week(timetable, year, week) -    for name, type_, groups, rooms, notes, begin, end in \ -        get_events(timetable, year, week, soup, weeks_in_soup): -        course = Course.objects.create(timetable=timetable, begin=begin, end=end) -        course.name = name -        course.type = type_ -        course.notes = notes - -        course.groups.add(*groups) -        if rooms is not None: -            course.rooms.add(*rooms) +    if year is not None and week is not None: +        delete_courses_in_week(timetable, year, week) +    else: +        Course.objects.filter(timetable=timetable, +                              begin__gte=min(weeks_in_soup.values())).delete() +    for course in get_events(timetable, soup, weeks_in_soup, year, week):          course.save() -    last_update.date = timezone.make_aware(datetime.datetime.now()) -    last_update.updated_at = new_update_date -    last_update.save() +    timetable.last_update_date = new_update_date +    timetable.save() -def process_timetable(timetable, year, weeks): +def process_timetable(timetable, force, year=None, weeks=None):      soup = get_xml(timetable.url)      weeks_in_soup = get_weeks(soup) -    for week in weeks: -        process_timetable_week(timetable, year, week, soup, weeks_in_soup) +    if year is not None and weeks is not None: +        for week in weeks: +            process_timetable_week(timetable, soup, weeks_in_soup, force, year, week) +    else: +        process_timetable_week(timetable, soup, weeks_in_soup, force)  class Command(BaseCommand):      help = "Fetches registered celcat timetables"      def add_arguments(self, parser): +        parser.add_argument("--all", const=True, default=False, action="store_const") +        parser.add_argument("--force", const=True, default=False, action="store_const")          parser.add_argument("--week", type=int, choices=range(1, 54), nargs="+")          parser.add_argument("--year", type=int, nargs=1) @@ -73,7 +77,9 @@ class Command(BaseCommand):          year = None          errcount = 0 -        if options["week"] is None: +        if options["all"]: +            weeks = None +        elif options["week"] is None:              _, week, day = timezone.now().isocalendar()              if day >= 6:                  year, week, _ = (timezone.now() + datetime.timedelta(weeks=1)).isocalendar() @@ -81,18 +87,20 @@ class Command(BaseCommand):          else:              weeks = options["week"] -        if options["year"] is None and year is None: -            year = timezone.now().year -        elif year is None: -            year = options["year"][0] +        if not options["all"]: +            if options["year"] is None and year is None: +                year = timezone.now().year +            elif year is None: +                year = options["year"][0]          for timetable in Timetable.objects.all():              self.stdout.write("Processing {0}".format(timetable))              try: -                process_timetable(timetable, year, weeks) -            except Exception as e: -                self.stderr.write(self.style.ERROR("Failed to process {0}: {1}".format(timetable, e))) +                process_timetable(timetable, options["force"], year, weeks) +            except Exception as exc: +                self.stderr.write( +                    self.style.ERROR("Failed to process {0}: {1}".format(timetable, exc)))                  errcount += 1          if errcount == 0: | 
