diff --git a/app/api/jury.py b/app/api/jury.py index 294eef2f6..9d4aad56b 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -10,7 +10,7 @@ import datetime -from flask import flash, g, request, url_for +from flask import g, request, url_for from flask_json import as_json from flask_login import current_user, login_required diff --git a/app/scodoc/sco_cal.py b/app/scodoc/sco_cal.py new file mode 100644 index 000000000..25179c016 --- /dev/null +++ b/app/scodoc/sco_cal.py @@ -0,0 +1,511 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Génération calendrier (ancienne présentation) +""" + +import calendar +import datetime +import html +import time + +from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError +from app.scodoc import sco_preferences +import app.scodoc.sco_utils as scu + + +def is_work_saturday(): + "Vrai si le samedi est travaillé" + return int(sco_preferences.get_preference("work_saturday")) + + +def MonthNbDays(month, year): + "returns nb of days in month" + if month > 7: + month = month + 1 + if month % 2: + return 31 + elif month == 2: + if calendar.isleap(year): + return 29 + else: + return 28 + else: + return 30 + + +class ddmmyyyy(object): + """immutable dates""" + + def __init__(self, date=None, fmt="ddmmyyyy", work_saturday=False): + self.work_saturday = work_saturday + if date is None: + return + try: + if fmt == "ddmmyyyy": + self.day, self.month, self.year = date.split("/") + elif fmt == "iso": + self.year, self.month, self.day = date.split("-") + else: + raise ValueError("invalid format spec. (%s)" % fmt) + self.year = int(self.year) + self.month = int(self.month) + self.day = int(self.day) + except ValueError: + raise ScoValueError("date invalide: %s" % date) + # accept years YYYY or YY, uses 1970 as pivot + if self.year < 1970: + if self.year > 100: + raise ScoInvalidDateError("Année invalide: %s" % self.year) + if self.year < 70: + self.year = self.year + 2000 + else: + self.year = self.year + 1900 + if self.month < 1 or self.month > 12: + raise ScoInvalidDateError("Mois invalide: %s" % self.month) + + if self.day < 1 or self.day > MonthNbDays(self.month, self.year): + raise ScoInvalidDateError("Jour invalide: %s" % self.day) + + # weekday in 0-6, where 0 is monday + self.weekday = calendar.weekday(self.year, self.month, self.day) + + self.time = time.mktime((self.year, self.month, self.day, 0, 0, 0, 0, 0, 0)) + + def iswork(self): + "returns true if workable day" + if self.work_saturday: + nbdays = 6 + else: + nbdays = 5 + if ( + self.weekday >= 0 and self.weekday < nbdays + ): # monday-friday or monday-saturday + return 1 + else: + return 0 + + def __repr__(self): + return "'%02d/%02d/%04d'" % (self.day, self.month, self.year) + + def __str__(self): + return "%02d/%02d/%04d" % (self.day, self.month, self.year) + + def ISO(self): + "iso8601 representation of the date" + return "%04d-%02d-%02d" % (self.year, self.month, self.day) + + def next_day(self, days=1): + "date for the next day (nota: may be a non workable day)" + day = self.day + days + month = self.month + year = self.year + + while day > MonthNbDays(month, year): + day = day - MonthNbDays(month, year) + month = month + 1 + if month > 12: + month = 1 + year = year + 1 + return self.__class__( + "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday + ) + + def prev(self, days=1): + "date for previous day" + day = self.day - days + month = self.month + year = self.year + while day <= 0: + month = month - 1 + if month == 0: + month = 12 + year = year - 1 + day = day + MonthNbDays(month, year) + + return self.__class__( + "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday + ) + + def next_monday(self): + "date of next monday" + return self.next_day((7 - self.weekday) % 7) + + def prev_monday(self): + "date of last monday, but on sunday, pick next monday" + if self.weekday == 6: + return self.next_monday() + else: + return self.prev(self.weekday) + + def __cmp__(self, other): # #py3 TODO à supprimer + """return a negative integer if self < other, + zero if self == other, a positive integer if self > other""" + return int(self.time - other.time) + + def __eq__(self, other): + return self.time == other.time + + def __ne__(self, other): + return self.time != other.time + + def __lt__(self, other): + return self.time < other.time + + def __le__(self, other): + return self.time <= other.time + + def __gt__(self, other): + return self.time > other.time + + def __ge__(self, other): + return self.time >= other.time + + def __hash__(self): + "we are immutable !" + return hash(self.time) ^ hash(str(self)) + + +# d = ddmmyyyy( '21/12/99' ) +def DateRangeISO(date_beg, date_end, workable=1): + """returns list of dates in [date_beg,date_end] + workable = 1 => keeps only workable days""" + if not date_beg: + raise ScoValueError("pas de date spécifiée !") + if not date_end: + date_end = date_beg + r = [] + work_saturday = is_work_saturday() + try: + cur = ddmmyyyy(date_beg, work_saturday=work_saturday) + end = ddmmyyyy(date_end, work_saturday=work_saturday) + except (AttributeError, ValueError) as e: + raise ScoValueError("date invalide !") from e + while cur <= end: + if (not workable) or cur.iswork(): + r.append(cur) + cur = cur.next_day() + + return [x.ISO() for x in r] + + +def day_names(): + """Returns week day names. + If work_saturday property is set, include saturday + """ + if is_work_saturday(): + return scu.DAY_NAMES[:-1] + else: + return scu.DAY_NAMES[:-2] + + +def next_iso_day(date): + "return date after date" + d = ddmmyyyy(date, fmt="iso", work_saturday=is_work_saturday()) + return d.next_day().ISO() + + +def YearTable( + year, + events=[], + firstmonth=9, + lastmonth=7, + halfday=0, + dayattributes="", + pad_width=8, +): + """Generate a calendar table + events = list of tuples (date, text, color, href [,halfday]) + where date is a string in ISO format (yyyy-mm-dd) + halfday is boolean (true: morning, false: afternoon) + text = text to put in calendar (must be short, 1-5 cars) (optional) + if halfday, generate 2 cells per day (morning, afternoon) + """ + T = [ + '' + ] + T.append("") + month = firstmonth + while 1: + T.append('") + if month == lastmonth: + break + month = month + 1 + if month > 12: + month = 1 + year = year + 1 + T.append("
') + T.append(MonthTableHead(month)) + T.append( + MonthTableBody( + month, + year, + events, + halfday, + dayattributes, + is_work_saturday(), + pad_width=pad_width, + ) + ) + T.append(MonthTableTail()) + T.append("
") + return "\n".join(T) + + +# ------ HTML Calendar functions (see YearTable function) + +# MONTH/DAY NAMES: + +MONTHNAMES = ( + "Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Aout", + "Septembre", + "Octobre", + "Novembre", + "Décembre", +) + +MONTHNAMES_ABREV = ( + "Jan.", + "Fév.", + "Mars", + "Avr.", + "Mai ", + "Juin", + "Juil", + "Aout", + "Sept", + "Oct.", + "Nov.", + "Déc.", +) + +DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") + +DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D") + +# COLORS: + +WHITE = "#FFFFFF" +GRAY1 = "#EEEEEE" +GREEN3 = "#99CC99" +WEEKDAYCOLOR = GRAY1 +WEEKENDCOLOR = GREEN3 + + +def MonthTableHead(month): + color = WHITE + return """ + \n""" % ( + color, + MONTHNAMES_ABREV[month - 1], + ) + + +def MonthTableTail(): + return "
%s
\n" + + +def MonthTableBody( + month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8 +): + firstday, nbdays = calendar.monthrange(year, month) + localtime = time.localtime() + current_weeknum = time.strftime("%U", localtime) + current_year = localtime[0] + T = [] + # cherche date du lundi de la 1ere semaine de ce mois + monday = ddmmyyyy("1/%d/%d" % (month, year)) + while monday.weekday != 0: + monday = monday.prev() + + if work_saturday: + weekend = ("D",) + else: + weekend = ("S", "D") + + if not halfday: + for d in range(1, nbdays + 1): + weeknum = time.strftime( + "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y") + ) + day = DAYNAMES_ABREV[(firstday + d - 1) % 7] + if day in weekend: + bgcolor = WEEKENDCOLOR + weekclass = "wkend" + attrs = "" + else: + bgcolor = WEEKDAYCOLOR + weekclass = "wk" + str(monday).replace("/", "_") + attrs = trattributes + color = None + legend = "" + href = "" + descr = "" + # event this day ? + # each event is a tuple (date, text, color, href) + # where date is a string in ISO format (yyyy-mm-dd) + for ev in events: + ev_year = int(ev[0][:4]) + ev_month = int(ev[0][5:7]) + ev_day = int(ev[0][8:10]) + if year == ev_year and month == ev_month and ev_day == d: + if ev[1]: + legend = ev[1] + if ev[2]: + color = ev[2] + if ev[3]: + href = ev[3] + if len(ev) > 4 and ev[4]: + descr = ev[4] + # + cc = [] + if color is not None: + cc.append('' % color) + else: + cc.append('') + + if href: + href = 'href="%s"' % href + if descr: + descr = 'title="%s"' % html.escape(descr, quote=True) + if href or descr: + cc.append("" % (href, descr)) + + if legend or d == 1: + if pad_width is not None: + n = pad_width - len(legend) # pad to 8 cars + if n > 0: + legend = ( + " " * (n // 2) + legend + " " * ((n + 1) // 2) + ) + else: + legend = " " # empty cell + cc.append(legend) + if href or descr: + cc.append("") + cc.append("") + cell = "".join(cc) + if day == "D": + monday = monday.next_day(7) + if ( + weeknum == current_weeknum + and current_year == year + and weekclass != "wkend" + ): + weekclass += " currentweek" + T.append( + '%d%s%s' + % (bgcolor, weekclass, attrs, d, day, cell) + ) + else: + # Calendar with 2 cells / day + for d in range(1, nbdays + 1): + weeknum = time.strftime( + "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y") + ) + day = DAYNAMES_ABREV[(firstday + d - 1) % 7] + if day in weekend: + bgcolor = WEEKENDCOLOR + weekclass = "wkend" + attrs = "" + else: + bgcolor = WEEKDAYCOLOR + weekclass = "wk" + str(monday).replace("/", "_") + attrs = trattributes + if ( + weeknum == current_weeknum + and current_year == year + and weekclass != "wkend" + ): + weeknum += " currentweek" + + if day == "D": + monday = monday.next_day(7) + T.append( + '%d%s' + % (bgcolor, weekclass, attrs, d, day) + ) + cc = [] + for morning in (True, False): + color = None + legend = "" + href = "" + descr = "" + for ev in events: + ev_year = int(ev[0][:4]) + ev_month = int(ev[0][5:7]) + ev_day = int(ev[0][8:10]) + if ev[4] is not None: + ev_half = int(ev[4]) + else: + ev_half = 0 + if ( + year == ev_year + and month == ev_month + and ev_day == d + and morning == ev_half + ): + if ev[1]: + legend = ev[1] + if ev[2]: + color = ev[2] + if ev[3]: + href = ev[3] + if len(ev) > 5 and ev[5]: + descr = ev[5] + # + if color is not None: + cc.append('' % (color)) + else: + cc.append('') + if href: + href = 'href="%s"' % href + if descr: + descr = 'title="%s"' % html.escape(descr, quote=True) + if href or descr: + cc.append("" % (href, descr)) + if legend or d == 1: + n = 3 - len(legend) # pad to 3 cars + if n > 0: + legend = ( + " " * (n // 2) + legend + " " * ((n + 1) // 2) + ) + else: + legend = "   " # empty cell + cc.append(legend) + if href or descr: + cc.append("") + cc.append("\n") + T.append("".join(cc) + "") + return "\n".join(T) diff --git a/sco_version.py b/sco_version.py index 2c6860862..86efcdf88 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.11" +SCOVERSION = "9.6.12" SCONAME = "ScoDoc"