Passage aux adhésions glissantes (partie I, sans lc_ldap)

This commit is contained in:
Pierre-Elliott Bécue 2014-08-15 20:26:12 +02:00
parent 169b7000b8
commit 49cef4c095
12 changed files with 485 additions and 231 deletions

View file

@ -17,6 +17,7 @@ import os
import random
import string
import time
import datetime
import sys
import pwd
import errno
@ -25,15 +26,17 @@ import ldap.modlist
import ldap_passwd
import netaddr
import time
import annuaires_pg as annuaires
import config
import config.impression
import config.cotisation as cotisation
import iptools
import ip6tools
import cPickle
import config_mail
from chgpass import change_password
from calendar import monthrange
from affich_tools import coul, prompt, cprint
from email_tools import send_email
from syslog import openlog, closelog, syslog
@ -151,6 +154,27 @@ def decode(s):
else:
return s.decode('utf-8', 'ignore') # On ignore les erreurs
def tz(thetz):
abstz = 100*abs(thetz)
if thetz == 0:
return "Z"
else:
return "%s%04d" % ("+"*(thetz < 0) + "-"*(thetz > 0), abstz)
def generalizedTimeFormat(stamp):
"""Converts a timestamp (local) in a generalized time format
for LDAP
"""
return "%s%s" % (time.strftime("%Y%m%d%H%M%S", time.localtime(stamp)), tz(time.altzone/3600))
def fromGeneralizedTimeFormat(gtf):
"""Converts a GTF stamp to unix timestamp
"""
return time.mktime(time.strptime(gtf.split("-", 1)[0].split("+", 1)[0].split('Z', 1)[0], "%Y%m%d%H%M%S"))
def strip_accents(a, sois_un_porc_avec_les_espaces = True):
""" Supression des accents de la chaîne fournie """
res = normalize('NFKD', decode(a)).encode('ASCII', 'ignore')
@ -392,7 +416,8 @@ class CransLdap:
'historique', 'blacklist', 'droits', 'uidNumber', 'info',
'solde', 'controle', 'contourneGreylist', 'rewriteMailHeaders',
'ablacklist', 'homepageAlias', 'charteMA',
'adherentPayant', 'gpgFingerprint'], \
'adherentPayant', 'gpgFingerprint', 'debutConnexion', 'finConnexion',
'debutAdhesion', 'finAdhesion'], \
'club': \
['cid', 'responsable', 'paiement', 'historique', 'blacklist',
'mailAlias', 'info', 'controle', 'ablacklist', 'imprimeurClub'], \
@ -401,7 +426,7 @@ class CransLdap:
'borneWifi': non_auto_search_machines_champs + \
['prise', 'puissance', 'canal', 'hotspot', 'positionBorne', 'nvram'],
'machineWifi': non_auto_search_machines_champs + ['ipsec'],
'facture': ['fid']}
'facture': ['fid', 'debutConnexion', 'finConnexion', 'debutAdhesion', 'finAdhesion']}
# tous les champs de recherche
search_champs = {}
@ -691,7 +716,6 @@ class CransLdap:
except:
raise ValueError(u"Impossible de créer l'objet %s" % nom_classe)
def search(self, expression, mode=''):
"""
Recherche dans la base LDAP, expression est une chaîne :
@ -757,13 +781,13 @@ class CransLdap:
# définifif (cf config.py).
if config.periode_transitoire:
# Pour la période transitoire année précédente ok
el = "(|(paiement=%d)(paiement=%d))" % (ann_scol, ann_scol-1)
el = "(|(paiement=%d)(paiement=%d)(finAdhesion>=%s))" % (ann_scol, ann_scol-1, generalizedTimeFormat(time.time()))
else:
el = "(paiement=%s)" % ann_scol
el = "(|(paiement=%s)(finAdhesion>=%s))" % (ann_scol, generalizedTimeFormat(time.time()))
# Doit-on bloquer en cas de manque de la carte d'etudiant ?
# (si période transitoire on ne bloque dans aucun cas)
if config.bl_carte_et_definitif:
el = "(&(|(carteEtudiant=%d)(objectClass=club))%s)" % (ann_scol, el)
el = "(&(|(carteEtudiant=%d)(objectClass=club)(carteEtudiant=TRUE))%s)" % (ann_scol, el)
elif champ[1:] == 'blacklist':
el = '(blacklist=%s)' % expr
else:
@ -1029,9 +1053,9 @@ class BaseClasseCrans(CransLdap):
# Il faut aussi regarder la blackliste du propriétaire
p = self.proprietaire()
bl_liste += p.blacklist()
elif isinstance(self, Adherent) and (config.ann_scol in self.paiement()):
elif isinstance(self, Adherent) and (config.ann_scol in self.paiement() or (self.adhesion() > time.time() and self.connexion() > time.time())):
# blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides
if not config.periode_transitoire and config.bl_carte_et_actif and not (config.ann_scol in self.carteEtudiant()) and not self.sursis_carte():
if not config.periode_transitoire and config.bl_carte_et_actif and not bool(self.carteEtudiant()) and not self.sursis_carte():
actifs['carte_etudiant']=('-','-')
if self.chbre() == '????':
actifs['chambre_invalide']=('-','-')
@ -1144,9 +1168,7 @@ class BaseClasseCrans(CransLdap):
[ index de la remarque à modifier, nouvelle remarque ]
l'index est celui obtenu dans la liste retournée par info()
"""
if not self._data.has_key('info'):
self._data['info'] = []
liste = list(self._data['info'])
liste = list(self._data.get('info', []))
if new == None: return map(decode, liste)
if type(new) == list:
@ -1180,8 +1202,12 @@ class BaseClasseCrans(CransLdap):
return []
if not self.dn:
# Enregistrement à placer en tête de base
self.dn = self.base_dn
# Enregistrement à placer en tête de base sauf si
# facture.
if isinstance(self, Facture):
self.dn = self.proprietaire().dn
else:
self.dn = self.base_dn
# Construction de l'historique
if not self._init_data:
@ -1209,7 +1235,8 @@ class BaseClasseCrans(CransLdap):
'puissance', 'canal', 'prise', 'responsable',
'macAddress', 'ipHostNumber', 'ip6HostNumber',
'host', 'positionBorne', 'derniereConnexion',
'hotspot', 'dnsIpv6', 'machineAlias']:
'hotspot', 'dnsIpv6', 'machineAlias', 'finAdhesion',
'finConnexion', 'debutConnexion', 'debutAdhesion']:
if champ in self.modifs:
if champ not in self._init_data.keys():
valeur_initiale = 'N/A'
@ -1227,7 +1254,9 @@ class BaseClasseCrans(CransLdap):
for champ in ['droits', 'controle', 'paiement', 'carteEtudiant',
'mailAlias', 'hostAlias', 'exempt', 'nvram',
'portTCPin', 'portTCPout', 'portUDPin', 'portUDPout',
'homepageAlias', 'imprimeurClub', 'gpgFingerprint']:
'homepageAlias', 'imprimeurClub', 'gpgFingerprint',
'debutConnexion', 'finConnexion', 'debutAdhesion',
'finAdhesion']:
if champ in self.modifs:
if champ == 'controle':
# Ce n'est pas pareil que self._init_data.get('controle', [''])
@ -1461,6 +1490,46 @@ class BaseProprietaire(BaseClasseCrans):
self._init_data = {}
self._modifiable = 'w'
def adhesion(self, update=False, f=None):
"""
Gestion de l'adhésion d'un adhérent
La durée d'adhésion ne peut être choisie
* Si update vaut True, on ajoute un an
* f est une facture passée en référence.
"""
thetime = time.time()
# On récupère sur les factures l'ensemble de celles comportant une adhésion.
adh_factures = self.factures_adh()
finAdh = max([0.0] + [fromGeneralizedTimeFormat(facture._data.get('finAdhesion', ["19700101000000Z"])[0]) for facture in adh_factures if facture.controle() != "FALSE"])
if update == False:
return finAdh
else:
ftime = datetime.datetime.fromtimestamp(max(finAdh, thetime))
if not finAdh - thetime < cotisation.delai_readh:
raise EnvironmentError, u"On ne peut réadhérer que 15 jours avant l'expiration de l'adhésion précédente."
# Calcul de la nouvelle date de fin d'adhésion.
# le +86400 est une souplesse pour permettre au câblage de se passer sans warning
# quand le mec se fait câbler pour un an.
newFinAdh = time.mktime(ftime.replace(year=ftime.year + cotisation.duree_adh_an).timetuple()) + 86400
# Si aucune facture n'est passée en référence, on en crée une nouvelle.
if f is None:
f = Facture(self)
if isinstance(self, Adherent):
f.ajoute(cotisation.dico_adh)
elif isinstance(self, Club):
f.ajoute(cotisation.dico_adh_club)
f._set("finAdhesion", [generalizedTimeFormat(newFinAdh)])
f._set("debutAdhesion", [generalizedTimeFormat(thetime)])
self._set("finAdhesion", self._data.get("finAdhesion", []) + [generalizedTimeFormat(newFinAdh)])
self._set("debutAdhesion", self._data.get("debutAdhesion", []) + [generalizedTimeFormat(thetime)])
return f
def droits(self, droits=None, light=False):
""" Renvoie les droits courants. Non modifiable (sauf si surchargée dans classe enfant)"""
if droits <> None:
@ -1543,6 +1612,7 @@ class BaseProprietaire(BaseClasseCrans):
if not isadm() and isadm(self.compte()):
raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')
else:
if type(new) == list:
# Modif
@ -1651,18 +1721,30 @@ class BaseProprietaire(BaseClasseCrans):
else:
return []
def factures(self):
def factures(self, filtre=None):
""" Retourne les factures (instances) appartenant à la classe """
# Le champ id n'est pas initialisé lorsque le proprio est en cours
# de création
if filtre is None:
filtre = Facture.filtre_idn
else:
filtre = "(&%s%s)" % (filtre, Facture.filtre_idn)
if self.id():
res = []
for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, Facture.filtre_idn):
for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, filtre):
res.append(self.make(r, self._modifiable))
return res
else:
return []
def factures_adh(self):
""" Retourne les factures pour adhésion """
return self.factures("(debutAdhesion=*)")
def factures_conn(self):
""" Retourne les factures pour connexion """
return self.factures("(debutConnexion=*)")
def solde(self, operation=None, comment=None):
""" Retourne ou modifie le solde d'un propriétaire
operation doit être un nombre positif ou négatif
@ -1825,8 +1907,12 @@ class BaseProprietaire(BaseClasseCrans):
return False
def paiement_ok(self):
if config.ann_scol in self.paiement() or (config.periode_transitoire and (config.ann_scol-1) in self.paiement()):
if config.periode_transitoire or not isinstance(self, Adherent) or not config.bl_carte_et_definitif or config.ann_scol in self.carteEtudiant():
if isinstance(self, Adherent):
m_paiement = min(self.adhesion(), self.connexion())
else:
m_paiement = self.adhesion()
if config.ann_scol in self.paiement() or (config.periode_transitoire and (config.ann_scol-1) in self.paiement()) or (m_paiement > time.time()) or (config.periode_transitoire and config.debut_periode_transitoire <= m_paiement <= config.fin_periode_transitoire):
if config.periode_transitoire or not isinstance(self, Adherent) or not config.bl_carte_et_definitif or bool(self.carteEtudiant()):
return True
else:
return self.sursis_carte()
@ -2281,6 +2367,47 @@ class Adherent(BaseProprietaire):
# renvoie la valeur trouvée dans la base
return bool(self._data.get('charteMA', []))
def connexion(self, mois=None, f=None):
"""
Gestion de la connexion d'un adhérent
* valeur est un entier définissant un nombre de mois
* f est une facture
"""
thetime = time.time()
# On récupère sur les factures l'ensemble de celles comportant une connexion.
conn_factures = self.factures_conn()
finConn = max([0.0] + [fromGeneralizedTimeFormat(facture._data.get('finConnexion', ["19700101000000Z"])[0]) for facture in conn_factures if facture.controle() != "FALSE"])
if mois is None:
return finConn
elif not isinstance(mois, int):
raise ValueError, u"Le nombre de mois doit être un entier"
else:
ftime = max(finConn, thetime)
# Calcul de la nouvelle date de fin d'adhésion.
curyear = datetime.datetime.now().year
curmonth = datetime.datetime.now().month
nbJours = 0
for i in xrange(1, mois+1):
nbJours += monthrange((curmonth + i - 1)/12 + curyear, (curmonth + i - 1)%12 + 12 * ((curmonth + i - 1) % 12 == 0))[1]
# On ajoute 3600 secondes sur suggestion de Raphaël Bonaque (<bonaque@crans.org>), pour tenir compte des malheureux qui
# pourraient subir le changement d'heure.
newFinConn = ftime + 86400 * nbJours + 3600
# Si aucune facture n'est passée en référence, on en crée une nouvelle.
if f is None:
f = Facture(self)
f.ajoute(cotisation.dico_cotis(mois))
f._set("finConnexion", [generalizedTimeFormat(newFinConn)])
f._set("debutConnexion", [generalizedTimeFormat(thetime)])
f._set("finConnexion", self._data.get("finConnexion", []) + [generalizedTimeFormat(newFinConn)])
f._set("debutConnexion", self._data.get("debutConnexion", []) + [generalizedTimeFormat(thetime)])
return f
def adherentPayant(self, valeur = None):
"""
L'adhérent paie sa cotisation (a droit au WiFi, à un compte Crans, ... True par défaut
@ -2354,7 +2481,11 @@ class Adherent(BaseProprietaire):
si positif ajoute l'année à la liste
si négatif le supprime
"""
return self._an('carteEtudiant', action)
if action == True:
self._set('carteEtudiant', ['TRUE'])
elif action == False:
self._set('carteEtudiant', [])
return bool(self._data.get('carteEtudiant', []))
def checkPassword(self, password):
"""Vérifie le mot de passe de l'adhérent"""
@ -3803,7 +3934,7 @@ class Facture(BaseClasseCrans):
self._set('modePaiement', [new])
return decode(self._data.get('modePaiement', [None])[0])
return decode(self._data.get('modePaiement', [''])[0])
def recuPaiement(self, new=None):
"""
@ -3833,6 +3964,19 @@ class Facture(BaseClasseCrans):
# renvoie la valeur trouvée dans la base
return self._data.get("recuPaiement", [None])[0]
def controle(self, action=None):
if action is None:
return self._data.get("controle", [''])[0]
else:
if action == True:
self._set("controle", ["TRUE"])
elif action == False:
self._set("controle", ["FALSE"])
elif action == "":
self._set("controle", [])
else:
raise ValueError("Mauvaise valeur pour l'attribut controle : %r" % (repr(action),))
def _del_recu_paiement(self):
""" Pour test """
self._set("recuPaiement", [])
@ -3940,7 +4084,7 @@ class Facture(BaseClasseCrans):
# enregistre la nouvelle liste
self._articles(arts)
def supprime(self, supprime):
def supprime(self, supprime=None, pop=None):
"""Supprime un/des article(s) à la facture
arts est un article ou une liste d'articles
"""
@ -3951,6 +4095,9 @@ class Facture(BaseClasseCrans):
# charge la liste des articles
arts = self._articles()
if pop is not None:
_ = arts.pop()
# on supprime les anciens articles
if type(supprime)==dict:
supprime = [supprime]
@ -4062,6 +4209,10 @@ class _FakeProprio(CransLdap):
for r in res:
m.append(self.make(r))
return m
def adhesion(self):
return time.time() + 86400
def connexion(self):
return time.time() + 86400
class AssociationCrans(_FakeProprio):
""" Classe définissant l'assoce (pour affichage de ses machines) """