ScoDoc/app/scodoc/sco_cal.py

407 lines
12 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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 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_by_day: dict[str, list[dict]],
firstmonth=9,
lastmonth=7,
dayattributes="",
):
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
"""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)
"""
T = [
"""<table id="maincalendar" class="maincalendar"
border="3" cellpadding="1" cellspacing="1" frame="box">"""
]
T.append("<tr>")
month = firstmonth
while True:
T.append('<td valign="top">')
T.append(_month_table_head(month))
T.append(
_month_table_body(
month,
year,
events_by_day,
dayattributes,
is_work_saturday(),
)
)
T.append(
"""
</table>
</td>"""
)
if month == lastmonth:
break
month = month + 1
if month > 12:
month = 1
year = year + 1
T.append("</table>")
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&nbsp;",
"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 _month_table_head(month):
color = WHITE
return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
<tr bgcolor="{color}">
<td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
</tr>\n"""
def _month_table_body(
month,
year,
events_by_day: dict[str, list[dict]],
trattributes="",
work_saturday=False,
) -> str:
"""
events : [event]
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
"""
firstday, nbdays = calendar.monthrange(year, month)
localtime = time.localtime()
current_weeknum = time.strftime("%U", localtime)
current_year = localtime[0]
rows = []
# cherche date du lundi de la 1ere semaine de ce mois
monday = ddmmyyyy(f"1/{month}/{year}")
while monday.weekday != 0:
monday = monday.prev()
if work_saturday:
weekend = ("D",)
else:
weekend = ("S", "D")
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
)
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
# events this day ?
events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
color = None
ev_txts = []
for ev in events:
color = ev.get("color")
href = ev.get("href", "")
description = ev.get("description", "")
if href:
href = f'href="{href}"'
if description:
description = f"""title="{html.escape(description, quote=True)}" """
if href or description:
ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
else:
ev_txts.append(ev.get("title", "&nbsp;"))
#
cc = []
if color is not None:
cc.append(f'<td bgcolor="{color}" class="calcell">')
else:
cc.append('<td class="calcell">')
cc.append(f"{', '.join(ev_txts)}</td>")
cells = "".join(cc)
if day == "D":
monday = monday.next_day(7)
if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
weekclass += " currentweek"
rows.append(
f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
<td class="calday">{d}{day}</td>{cells}</tr>"""
)
return "\n".join(rows)