Code source de ServeurFonctions

# -*- coding: utf-8 -*-

"""Ce module a pour seul but d'être appelé par serveur.py
En effet, le code étant trop monolithique,
les fonctions sont définies ici et les méthodes de :py:class:`Server`
sont les fonctions de ce module.

*(Attention, de ce fait, une bonne partie de la documentation est dupliquée)*

Évidemment elles sont toutes appelées avec comme premier
paramètre self.

"""


## Imports
# useful
import os
import sys
import time
import datetime
import re
import copy
import inspect
import tempfile
# communication
import traceback
# pour encoder/décoder les photos et les pdf
import base64
# les objets échangés sont des objets json
import json
# pour effectuer des opérations comme le resize des photos
import subprocess

# communication
import netaddr
# module d'accès à la bdd
import psycopg2
import psycopg2.extras

# module qui a des accès à la base de données préprogrammés (car souvent utilisés)
import ReadDatabase
# Définition de la classe Compte
from BaseFonctions import Compte
# Import du fichier de conf
sys.path.append("../config/")
import config
# Module qui gère les envois de mails
sys.path.append("../mail/")
import mail
# module qui définit des fonctions essentielles
import BaseFonctions
# module qui définit les erreurs
import ExceptionsNote
# module qui assure la maintenance de la cohérence de la base de données
import Consistency
# module pour modifier le Wiki
import Wiki

### COMMANDES UTILITAIRES
[docs]def _has_acl(self, access, surdroit=False, sousdroit=False): """Vérifie que l'utilisateur a le droit ``access`` (ou le surdroit si ``surdroit``est ``True``).""" if not(self.isauth): # Si on n'est pas logué, on n'a aucun droit return False if self._has_timeouted(access): # Si le droit a timeouté, on répond non return False # Ensuite, c'est complètement différent si je suis un user special ou non if (self.userid == "special"): # Là, le cas se traite directement if surdroit: raise ExceptionsNote.TuTeFousDeMaGueule("on ne demande pas les surdroits d'un special user") aliases = config.droits_aliases_special result = BaseFonctions.inOrInAliasesWithMask(access, self.acl, aliases, self.masque) else: # Pour un user bdd c'est l'AuthService qui s'occuppe de ça result = self.auth.has_acl(self.userid, access, surdroit, self.masque, sousdroit=sousdroit) if result: # Si on a bien le droit, on met a jour son timestamp avant de confirmer self._refresh_session(access) return result if access == 'alive': # Si on a perdu le droit alive, il faut déloguer l'utilisateur""" self._kill_session() # Si on arrive là, c'est que result = false, on répond donc non return False
[docs]def _myself(self, idbde=None): """Vérifie si l'utilisateur courant a accès à ce compte parce que c'est le sien. NB : il doit aussi avoir le droit ``myself`` (comme ça on peut se l'enlever volontairement "pour laisser"). Appelé sans le paramètre idbde, répond simplement si l'utilisateur courant peut accéder à son propre compte. """ prerequis = self.userid != "special" and self._has_acl("myself") if idbde is None: return prerequis else: return prerequis and self.userid == idbde
[docs]def _kill_session(self): """Réinitialise les paramètres de session utilisateur. """ if (self.userid == "special"): key = self.username else: key = self.userid self.auth.last_action_timestamps[key] = {} self.isauth = False self.username = "" self.userid = None
[docs]def _refresh_session(self, droit): """Met à jour les timestamps de dernière action et parfois le cache de droits. Est appelée à chaque action effectuée par le client. """ if (self.userid == "special"): key = self.username else: key = self.userid # La première fois, il faut créer le dictionnaire if not self.auth.last_action_timestamps.has_key(key): self.auth.last_action_timestamps[key] = {} """ TODO not RC, pour le moment les droits sont en bordel. On voudra à la fin avoir plusieurs niveau de classification de droits, tels que sensible, trivial, normal. Si on met à jour le timestamp d'un droit sensible, le timestamp des niveaux moins sensibles doivent être mis à jours. Pour le moment, ceci n'est pas implémenté. Ainsi, si par exemple un utilisateur reste sur la page conso et fais des consos, uniquement le timeout du droit consos est mis à jour. Si l'utilisateur veut changer de page, il se fait jeter, car le timeout de alive est depuis longtemps expiré. Je propose pour le moment de systématiquement mettre à jour le timestamp de alive. """ self.auth.last_action_timestamps[key]["alive"] = time.time()
[docs]def _has_timeouted(self, droit="alive"): """Vérifie que l'utilisateur n'est pas inactif depuis trop longtemps pour être encore autorisé à utiliser le droit ``droit``. Avec ``droit = "alive"``, vérifie qu'il est encore authentifié. """ if (self.userid == "special"): key = self.username else: key = self.userid now = time.time() # Si le droit n'a pas de timeout particulier, on utilise par défaut celui de "alive" timeout = config.inactivity_timeout.get(droit, config.inactivity_timeout["alive"]) # Si le droit n'a encore jamais été utilisé, on va prendre le timestamp de alive comme référence if not self.auth.last_action_timestamps[key].has_key(droit): droit = "alive" if not self.auth.last_action_timestamps[key].has_key("alive"): # si on n'a jamais rien fait, alors on peut considérer qu'on n'a pas encore timouté return False last = self.auth.last_action_timestamps[key][droit] result = (now-last)>timeout return result
[docs]def _pasledroit(self, failacl): """Pour factoriser toutes les réponses "Tu n'as pas le droit...". """ self._send(None, 403, u"Tu n'as pas le droit %s." % (failacl,)) self._debug(3, u"Pas les droits %s." % (failacl,)) return
[docs]def _badparam(self, failcmd): """Pour factoriser toutes les réponses "Mauvais paramètre". """ self._send(None, 4, u"Mauvais paramètre pour %s." % (failcmd.strip("\n"),)) self._debug(3, u"Mauvais paramètre pour %s : %r" % (failcmd, self._last_param)) return
################################################################# ## Commandes standard ## #################################################################
[docs]def hello(self, data): """Réponse à un hello. * Transmet un ``"ok"`` si la version du client est acceptée. (Indispensable pour continuer la communication) * Sinon, transmet la liste des versions acceptées. * ``data = "<version du client>"`` * ``data = ["<version du client>", "<IP de l'utilisateur>"]`` Seules certaines IPs réelles sont de confiance et autorisées à "cafter" l'IP de l'utilisateur (notamment le client web) """ if not (isinstance(data, unicode) or (isinstance(data, list) and [type(i) for i in data] == [unicode, unicode])): _badparam(self, u"hello") return if isinstance(data, unicode): version = data else: version = data[0] clwh = config.clients_whitelist if version.lower() in [i.lower() for i in clwh]: self._debug(4, u'Version client "%s" : ok' % (version,)) self._send("Client ok.") self.client_version = version.lower() # comme ça on a mémorisé le client à qui on parle # et on le met aussi dans la liste des onlines self.auth.masterserver.online_list[self.idServer]["client"] = self.client_version # On override l'IP si il est dans les clients de confiance if self.ip in config.trusted_ips and isinstance(data, list): ip_user = data[1] try: ip = netaddr.IPAddress(ip_user) except netaddr.AddrFormatError: self._debug(3, u"Tentative d'override d'IP invalide : %s" % (ip_user,)) return self._debug(4, u"Override par l'ip utilisateur %s (%s trusted)" % (ip_user, self.ip)) self.ip = ip_user else: self._debug(3, u'Version client "%s" : pas ok' % (version,)) self._send(None, 11, u"Client non reconnu.\nClients acceptés : " + ", ".join(clwh) + u"\n") self.client_version = None # Si un débile refait un hello après un réussi... self.auth.masterserver.online_list[self.idServer]["client"] = None
[docs]def help(self): """Transmet la liste des commandes.""" # On prend la liste des méthodes d'un Server commands = [c[0] for c in inspect.getmembers(self, predicate=inspect.ismethod)] # On enlève celles qui commencent par un underscore commands = [c for c in commands if not c.startswith("_")] self._send(u"Commandes disponibles :\n" + u", ".join([c for c in commands]) + u"\n" + u"""(man "commande" fournit de l'aide sur une commande)\n""")
[docs]def man(self, commande): """Transmet de l'aide sur la commande.""" if not isinstance(commande, unicode): _badparam(self, u"man (donnez un nom de commande)") return if BaseFonctions.executable_cmd(commande): try: fonction = self.__getattribute__(commande) except AttributeError: pass # le message d'erreur est transmis plus bas else: self._debug(4, u"man sur %s." % (commande,)) self._send("\n".join([i.strip() for i in fonction.__doc__.split("\n")])) return self._debug(3, u"No manual entry for %s." % (commande,)) self._send(None, 16, u"No manual entry for %s." % (commande,))
[docs]def die(self): """Réponse à un die (arrêt du serveur).""" if self._has_acl('die'): self._debug(0, u"*** Die demandé par %s.***" % (self.username,)) self._send(u"Server killed.") self.auth.masterserver.stopping(u"die demandé par %s" % (self.username,)) # Bon, on devrait faire un certains nombre de trucs pour quitter proprement, # mais comme tout va exploser, on s'en fiche un peu... else: _pasledroit(self, "die")
[docs]def login(self, data): """``data = [<username>, <password>, "bdd"/"special", <masque>]`` Réponse à un login. ``<masque>`` = *acl qu'on ne* **veut pas** *avoir pour cette session.* * Pour un "special", c'est une liste de droits * Pour un "bdd", c'est une liste ``[liste_des_droits, liste_des_surdroits, bool_supreme]`` Vérifie que l'utilisateur existe et a les droits suffisants pour se loguer. Transmet les informations sur le compte si c'est le cas. C'est ici que sont rejetés les comptes limités à certaines IPs. """ if not((type(data) == list) and (len(data) == 4) and ([type(i) for i in data] == [unicode, unicode, unicode, list]) and data[2] in ["bdd", "special"] and (len(data[0]) > 0)): # pas de pseudo vide _badparam(self, u"login") return # check de la forme de masque if (data[2] == "bdd") and not([type(j) for j in data[3]] == [list, list, bool] and [type(k) for k in data[3][0] + data[3][1]] == [unicode]*( len(data[3][0]) + len(data[3][1])) ): _badparam(self, u'login (masque "bdd")') return elif (data[2] == "special") and not([type(j) for j in data[3]] == [unicode] * len(data[3])): _badparam(self, u'login (masque "special")') return user, password, auth_type, masque = data byidbde = False if (user[0] == "#") and (auth_type == "bdd"): # C'est donc qu'on cherche à se connecter avec son idbde try: user = int(user.replace('#', '')) except Exception: _badparam(self, u"login (tentative incorrecte d'utilisation du mode idbde : %s)" % (user,)) return byidbde = True # AuthService.login renvoie un booléen et : # [un objet Compte si c'est un login "bdd"] ou [la liste des acl pour un "special"] ok, userbdd_ou_acl = self.auth.login(user, password, auth_type, byidbde) if (auth_type == "special"): acl = userbdd_ou_acl else: userbdd = userbdd_ou_acl if ok: # AuthService.get_acl ne gère pas le masque, on considère que "login" ne peut pas être enlevé par un masque... if (auth_type == "special"): droits = acl aledroit = BaseFonctions.inOrInAliases("login", droits, config.droits_aliases_special) else: aledroit = (self.auth.has_acl(userbdd.idbde, "login") # sans précision il n'y a pas de masque appliqué or self.auth.has_acl(userbdd.idbde, "login", surdroit=True)) if aledroit: # On vérifie que le compte demandé n'est pas limité if auth_type == "bdd" and userbdd.idbde in config.limited_accounts.keys(): if not self.ip in config.limited_accounts [userbdd.idbde]: self._debug(3, u"Login refusé : le compte %s ne peut pas se connecter depuis l'IP %s" % (userbdd.idbde, self.ip)) self._send(None, 503, u"Login refusé : ton compte n'est pas autorisé à se connecter depuis cette IP.") return self.isauth = True if (auth_type == "special"): self.userid = "special" self.acl = acl # un user bdd ne les a pas, il doit les demander à AuthService self.username = user self.masque = masque else: self.userid = userbdd.idbde self.username = userbdd.pseudo self.masque = masque # On s'enregistre dans la liste des onlines # le MainServer est accessible par l'AuthService self.auth.masterserver.online_list[self.idServer]["username"] = self.username self.auth.masterserver.online_list[self.idServer]["userid"] = self.userid self.auth.masterserver.online_list[self.idServer]["client"] = self.client_version if (auth_type == "special"): self._send(acl) self._debug(4, u"Authentification de %s : ok" % (user,)) self._refresh_session("alive") else: self._send(userbdd.get_data(True)) # True, parce qu'on a le droit de voir ses propres pbsante self._debug(4, u"Authentification de %s : ok" % (userbdd.pseudo,)) self._refresh_session("alive") else: _pasledroit(self, "login") return else: self._debug(3, u"Authentification de %s : échec" % (user,)) self._send(None, 5, u"Authentification échouée.")
[docs]def adduser(self, data): """Ajoute/met à jour un utilisateur spécial. ``data = [<user>, <password>, <newacl>]`` (avec ``"-"`` en ``password`` pour le laisser inchangé). """ if not((type(data) == list) and (len(data) == 3) and ([type(i) for i in data] == [unicode, unicode, list]) and ([type(i) for i in data[2]] == [unicode] * len(data[2]))): _badparam(self, u"adduser") return if self._has_acl('adduser'): [user, password, newacl] = data self.auth.adduser(user, password, newacl) con, cur = BaseFonctions.getcursor() # On écrase le mdp pour pas le loguer data[1] = "*" self._log("adduser", cur, str(data)) cur.execute("COMMIT;") self._debug(1, u"adduser " + user.decode("utf-8")) self._send(u"adduser ok.") else: _pasledroit(self, "adduser")
[docs]def deluser(self, user): """Supprime un utilisateur spécial.""" if not(type(user) == unicode): _badparam(self, u"deluser") return if self._has_acl('deluser'): if not user in self.auth.get_logins().keys(): self._debug(3, u"Tentative de suppression d'un utilisateur inexistant : %s" % (user,)) self._send(None, 6, u"Cet utilisateur n'existe pas.") return if (user != self.username): self.auth.deluser(user) con, cur = BaseFonctions.getcursor() self._log("deluser", cur, user) cur.execute("COMMIT;") self._debug(1, u"deluser " + user.decode("utf-8")) self._send(u"deluser ok.") else: self._debug(3, u"%s a essayé de se supprimer lui-même." % (self.username,)) self._send(None, 7, u"Tu ne peux pas te supprimer toi-même !") else: _pasledroit(self, "deluser")
[docs]def myconnection(self): """Transmet les infos sur la connection courante.""" conn_list = self.auth.masterserver.online_list.copy() self._send(conn_list[self.idServer]) self._debug(4, u"Infos connection %s envoyées." % (self.idServer,))
[docs]def users(self): """Transmet la liste des utilisateurs spéciaux existants avec leurs droits.""" if self._has_acl('users'): self._send(self.auth.get_logins()) self._debug(4, u"Liste des utilisateurs spéciaux envoyée.") else: _pasledroit(self, "users")
[docs]def whowith(self, data): """Transmet la liste des utilisateurs connectés et leurs ip, port ( + username, userid). En appliquant un filtre conditionnel sur ces attributs. ``data = {"ip" : <listip>, "userid" : <listid>, "username" : <listnames>, "client" : <listversions>}``, chacune des clés pouvant être absente. Renvoie les utilisateurs tels que (``utilisateur.ip`` ∈ ``<listip>``) **ET** (``utilisateur.userid`` ∈ ``<listid>``) … """ # on vérifie qu'on n'a que des paramètres de recherche corrects if not ( (type(data) == dict) and set(data.keys()).issubset(["ip", "userid", "username", "client"])): _badparam(self, u"whowith") return champs = data.keys() # ces champs doivent être du bon type dicotypes = {"ip": [unicode], "userid": [unicode, int], "username": [unicode], "client": [unicode]} if not(all([ (type(data[cle]) == list) and all([type(j) in dicotypes[cle] for j in data[cle]]) for cle in data.keys()])): _badparam(self, u"whowith") return if self._has_acl("who"): # on redéfinit le "in" pour pouvoir préciser la fonction d'égalisation... def dans(e, l, egal): return sum([egal(e, i) for i in l]) > 0 # ... parce qu'elle est différente... egal_normal = lambda x, y: (x == y) # ... pour les string egal_str = lambda x, y: (unicode(x).lower() == unicode(y).lower()) # on les classe par type egaux = {int: egal_normal, unicode: egal_str, type(None): egal_str, str: egal_str} onlines = self.auth.masterserver.online_list.copy() answer = {} for i in onlines.keys(): # on exprime \forall cle \in cle_fournies, onlines[i][cle] \in_modifié data[cle] # NB : on a bien all([]) == True if all([dans(onlines[i][cle], data[cle], egaux[type(onlines[i][cle])]) for cle in data.keys()]): answer[i] = onlines[i] self._send(answer) self._debug(4, u"whowith avec %s" % (data,)) else: _pasledroit(self, "whowith")
[docs]def client_speak(self, data): """Envoie un message à un ``Server`` particulier. Peut être appelée par un client (special user only). """ if not ((type(data) == list) and (len(data) == 2) and (type(data[0]) == int)): _badparam(self, u"speak") return if self._has_acl("speak"): try: idServ, message = data prefixe = "Message from %s (%s) :\n" % (self.username, self.idServer) message = prefixe + json.dumps(message) self.auth.masterserver.speak(idServ, message) self._debug(4, u"speak (done) : %s" % (data,)) self._send("speak done.") except ExceptionsNote.NoSuchServer: self._send(None, 8, u"Ce client n'existe pas ou a été déconnecté.") self._debug(3, u"speak (failed) : %s" % (data,)) except: self._debug(0, u"This should not be happening !\n(ServeurFonctions, try to speak)") else: _pasledroit(self, "speak")
[docs]def client_broadcast(self, message): """Envoie un message à tous les clients connectés. Peut être appelée par un client (special user only). """ if self._has_acl("broadcast"): prefixe = "Broadcast from %s (%s) :\n" % (self.username, self.idServer) message = prefixe + json.dumps(message) self.auth.masterserver.broadcast(message) self._debug(4, u"broadcast : %s" % (message,)) self._send("broadcast done.") else: _pasledroit(self, "broadcast")
################################################################# ## Commandes de note ## ################################################################# quick_search.__doc__ = "``data`` = une liste de 1 ou 2 éléments, contenant le terme recherché et éventuellement un flag ``o``, ``x`` ou ``ox``\n\n" + ReadDatabase.quick_search.__doc__ + "Transmet une liste de dictionnaires (un par résultat)."
[docs]def _handle_duplicate_account(self,prenom, nom, email, comptes, from_function): """Gère les problèmes commun aux deux fonctions suivantes.""" if len(comptes) > 1: mail.mail_generate_password_duplicate(prenom, nom, email, comptes) self._debug(3, u"%s a trouvé trop de [prenom, nom, mail] = %s (%s)" % (from_function, [prenom,nom,email], len(comptes))) self._send(None, 405, u'Il y a %s comptes qui correspondent à cette recherche. Demande annulée (les respo-info ont été prévenus du problème).' % (len(comptes))) else: self._debug(3, u"%s n'a pas trouvé de [prenom, nom, mail] = %s" % (from_function, [prenom,nom,email],)) self._send(None, 404, u'Pas de compte avec ces prénom, nom, mail.')
[docs]def generate_reset_password(self, data): """Envoie un mail contenant un lien permettant de réinitialiser le mot de passe du compte. ``data`` = [<prenom>, <nom>, <mail>] """ if not((type(data) == list) and len(data) == 3 and all([type(i)==unicode for i in data])): _badparam(self, u"generate_reset_password") return [prenom, nom, email] = data con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM comptes WHERE LOWER(prenom) = LOWER(%s) AND LOWER(nom) = LOWER(%s) AND LOWER(mail) = LOWER(%s);", (prenom, nom, email)) comptes = cur.fetchall() if len(comptes) == 1: compte = comptes[0] idbde = compte["idbde"] if compte["supreme"]: self._debug(3, u"generate_reset_password annulé car compte %s supreme" % (idbde)) self._send(None, 408, u"Demande annulée : ce compte a trop de droits.") return token, _ = BaseFonctions.random_chain(config.token_regenerate_password_size) while True: cur.execute("SELECT token FROM regen_password where token = %s;",(token,)) res = cur.fetchall() if len(res)==0: break else: token, _ = BaseFonctions.random_chain(config.token_regenerate_password_size) timestamp = datetime.datetime.now() # On récupère le délai de validité d'un token de régénération de mot de passe cur.execute("SELECT token_regenerate_password_delay FROM configurations WHERE used;") token_delay = cur.fetchone()[0] mail.mail_regenerate_password(prenom, nom, email, token, timestamp, token_delay) cur.execute("INSERT INTO regen_password (idbde, mail, token, timestamp) VALUES (%s, %s, %s, %s)", (idbde, email, token, timestamp.strftime("%F %T"))) # On n'appelle pas self._log parce que self.userid n'est pas forcément défini BaseFonctions.log(self.ip, "(not logged)", "generate_reset_password", cur, data) cur.execute("COMMIT;") self._debug(4, u"generate_reset_password de %s" % (idbde,)) self._send(u"Un mail a été envoyé à %s avec les instructions à suivre." % (email,)) else: self._handle_duplicate_account(prenom, nom, email, comptes, "generate_reset_password")
[docs]def confirm_reset_password(self, data): """Change le mot de passe grâce à un token reçu par mail. ``data`` = [<token>, <nouveau mot de passe>] _log relevant ids : idbde """ if not((type(data) == list) and len(data) == 2 and all([type(i)==unicode for i in data])): _badparam(self, u"generate_reset_password") return [given_token, new_password] = data con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM regen_password WHERE token = %s ORDER BY timestamp DESC;", (given_token,)) tokens = cur.fetchall() if tokens: # On prend le plus récent token = tokens[0] # On récupère les infos du comptes associé cur.execute("SELECT * FROM comptes WHERE idbde=%s;", [token["idbde"]]) comptes = cur.fetchall() compte = comptes[0] idbde = compte["idbde"] # On va vérifié qu'il n'est pas périmé cur.execute("SELECT token_regenerate_password_delay FROM configurations WHERE used;") token_delay = cur.fetchone()[0] if token["timestamp"] + datetime.timedelta(seconds=token_delay) > datetime.datetime.now(): old_pass = compte["passwd"] new_password = BaseFonctions.hash_pass(new_password) cur.execute("UPDATE comptes SET passwd = %s WHERE idbde = %s;", (new_password, idbde)) cur.execute("DELETE FROM regen_password WHERE token = %s;", (given_token,)) self._log("confirm_reset_password", cur, "reset password de %s, %s->%s" % (idbde, old_pass, new_password), [idbde]) cur.execute("COMMIT;") self._debug(1, u"confirm_reset_password de %s" % (idbde,)) self._send(u"Mot de passe modifié pour le compte : '%s'." %(compte["pseudo"],)) else: self._debug(3, u"token regen_password expiré pour le compte %s" % (idbde,)) self._send(None, 407, u"Ce lien de régénération de mot de passe est expiré.") else: self._debug(3, u"pas de token regen_password stocké pour ce token %s invalide" % (given_token,)) self._send(None, 406, u"Lien de régénération de mot de passe invalide.")
[docs]def confirm_email(self, data): """ Confirme l'adresse mail du compte grâce à un token reçu par mail. ``data`` = [<idbde>, <hash>] _log relevant ids : idbde """ if not((type(data) == list) and len(data) == 2 and type(data[0]) == int and type(data[1]) == unicode): _badparam(self, u"confirm_email") return [idbde, hash] = data con, cur = BaseFonctions.getcursor() cur.execute("SELECT mail, mail_token FROM comptes WHERE idbde = %s;", (idbde,)) mails = cur.fetchall() if mails: [mail, mail_token] = mails[0] hashed = BaseFonctions.hash_mail(mail, idbde) if hashed == hash: cur.execute("UPDATE comptes SET mail_token = 'validated' WHERE idbde = %s;", (idbde,)) self._log("confirm_email", cur, [idbde, hash], [idbde]) cur.execute("COMMIT;") self._debug(1, u"confirm_email de %s" % (idbde,)) self._send(u"Adresse e-mail confirmée.") else: self._debug(3, u"confirm_email %s : token don't match (received : %s, database : %s)" % (idbde, hash,)) self._send(None, 2456543, u"Code de confirmation e-mail incorrect. (Peut-être qu'un plus récent a été généré.)") else: self._debug(3, u"confirm_email : idbde %s non trouvé" % (idbde,)) self._send(None, 404, u"Compte %s inexistant." % (idbde,))
[docs]def get_display_info(self, data): """A une liste d'idbde renvoie un dictionnaire contenant pour chaque id le pseudo, le solde, et l'état du négatif""" if not((type(data) == list)): _badparam(self, u"get_display_info") return acl_quick_search, acl_dons = self._has_acl("quick_search"), self._has_acl("dons") if acl_quick_search or acl_dons: result = {} for idbe in data : compte =ReadDatabase.get_display_info(idbe) result[idbe] = {} result[idbe]["negatif"]= compte["negatif"] result[idbe]["pseudo"]= compte["pseudo"] result[idbe]["solde"]= compte["solde"] self._send(result) else: _pasledroit(self, "search")
get_display_info.__doc__ = "``data`` = une liste d'idbde``o``\n\n"
[docs]def historique_pseudo(self, data): """Transmet la liste des anciens pseudos, même ceux qui ne référencent plus le compte.""" if not(type(data) == int): _badparam(self, u"historique_pseudo") return idbde = data if self._has_acl("historique_pseudo") or self._myself(idbde): con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM historique WHERE idbde = %s ORDER BY date;", (idbde,)) liste = cur.fetchall() self._send(liste) self._debug(4, u"envoi de la liste des anciens pseudo de %s" % (idbde,)) else: _pasledroit(self, "historique_pseudo")
[docs]def search_historique_pseudo(self, data): """Effectue une recherche dans les historiques de pseudos, même ceux qui ne référencent plus le compte. ``data = [<terme de recherche>, <filter "b", "x" ou "">]`` Transmet la liste de ceux qui ont matché. """ if not ((type(data) == list) and ([type(i) for i in data] == [unicode, unicode]) and data[1] in ["", "b", "x"]): _badparam(self, u"search_historique_pseudo") return terme, exactfilter = data if self._has_acl("search"): con, cur = BaseFonctions.getcursor() # _ et % étant des caractères spéciaux pour LIKE, on a besoin de les échapper, # on choisit par convention de les échapper avec =, qu'on doit donc lui-même échapper (fais au moins semblant de suivre ce que je dis) # la syntaxe est "LIKE '%truc%' ESCAPE '='" terme = terme.replace('=', '==').replace('%', '=%').replace('_', '=_') if (exactfilter == "b"): terme += "%" elif (exactfilter == ""): terme = "%" + terme + "%" cur.execute("""SELECT c.idbde, c.nom, c.prenom, c.pseudo, c.solde, c.mail, section(c.idbde) AS section, h.avant AS match FROM comptes AS c, historique AS h WHERE c.idbde = h.idbde AND h.avant ILIKE %s ESCAPE '=';""", (terme,)) l = cur.fetchall() self._debug(4, u"search_historique_pseudo avec %s" % (data,)) self._send([dict(i) for i in l]) else: _pasledroit(self, "search")
[docs]def chgpass(self, data): """Change le mot de passe d'un compte. ``data = [<idbde>, <nouveau mdp>]`` *(non hashé)* _log relevant ids : idbde """ if not((type(data) == list) and (len(data) == 2) and ([type(i) for i in data] == [int, unicode]) and (data[0] > 0)): _badparam(self, u"chgpass") return idbde, new_pass = data if self._has_acl("chgpass") or self._myself(idbde): target = Compte(ReadDatabase.get_compte(idbde)) if (target.supreme and (type(self.userid) == int) and (self.userid > 0) and not(Compte(ReadDatabase.get_compte(self.userid)).supreme) ): self._debug(3, u"chgpass : Non-suprême ne peut pas changer le mdp d'un suprême") self._send(None, 402, u"Tu n'as pas les droits nécessaires pour changer ce mot de passe.") return # Là, c'est bon, du coup il faut faire un hash correct de tout ça new_pass = BaseFonctions.hash_pass(new_pass) con, cur = BaseFonctions.getcursor() cur.execute("UPDATE comptes SET passwd = %s WHERE idbde = %s;", (new_pass, idbde)) old_pass = target.passwd self._log("chgpass", cur, "password de %s, %s->%s" % (idbde, old_pass, new_pass), [idbde]) cur.execute("COMMIT;") self._debug(1, u"chgpass de %s" % (idbde,)) self._send(u"Mot de passe modifié.") else: _pasledroit(self, "chgpass")
[docs]def update_compte(self, data): """``data = <un dictionnaire>`` Modifie le compte. On ne fournit que les champs qu'on cherche à modifier. Les champs possibles sont : * ``"idbde"`` : **ne peut pas être modifié**, sert à identifier le compte qu'on cherche à modifier. * ``"type"`` : ``"personne"`` ou ``"club"`` * ``"pseudo"`` : pseudonyme du compte * ``"passwd"`` : **ne peut pas être modifié**, utiliser :py:meth:`Server.chgpass` * ``"nom"`` : nom de famille de l'adhérent * ``"prenom"`` : prénom de l'adhérent * ``"tel"`` : numéro de téléphone * ``"mail"`` : adresse e-mail pour joindre le détenteur du compte * ``"adresse"`` : adresse de l'adhérent/local du club * ``"fonction"`` : fonction au sein du BDE * ``"normalien"`` : vrai si l'adhérent est normalien (n'a pas vraiment de sens pour les clubs) * ``"pbsante"`` : problème de santé ou allergie alimentaire particulièr-e dont le BDE devrait avoir connaissance (pour le WEI, par exemple) * ``"droits"`` : liste des droits * ``"surdroits"`` : liste des surdroits * ``"report_period"`` : durée (en minutes) entre deux rapports. (-1 = jamais, 0 = dès qu'une transaction a lieu) * ``"next_report_date"`` : date du prochain rapport mail (cf :py:func:`BaseFonctions.isPgsqlDate` pour le format de date) * ``"bloque"`` : si vrai, le compte ne peut plus effectuer de transactions * ``"section"`` : section pour l'année en cours * ``"commentaire"`` : random garbage _log relevant ids : idbde """ if not ((type(data) == dict) and ("idbde" in data.keys()) and ([type(i) for i in data.keys()] == [unicode] * len(data.keys()))): _badparam(self, u"update_compte") return fields = data.keys() dicotypes = {"idbde": int, "type": unicode, "pseudo": unicode, "passwd": unicode, "nom": unicode, "prenom": unicode, "tel": unicode, "mail": unicode, "adresse": unicode, "fonction": unicode, "normalien": bool, "pbsante": unicode, "droits": unicode, "surdroits": unicode, "report_period": int, "next_report_date": unicode, "bloque": bool, "commentaire": unicode, "section": unicode} champs_autorises = dicotypes.keys() if not(set(fields).issubset(champs_autorises)): self._debug(3, u"Mauvais paramètre pour update_compte : champs inexistants : %s" % ([champ for champ in fields if not champ in champs_autorises],)) self._send(None, 4, u"Mauvais paramètre : champ(s) inexistant(s).") return # Désolé, mais on ne touche pas au comptes Spéciaux if (data["idbde"] < 0): self._send(None, 4, u"Les comptes spéciaux ne sont pas modifiables.") self._debug(3, u"update_compte failed : tentative de modification d'un idbde<0 (%s)" % (data["idbde"],)) return # On récupère la cible, on l'updatera comme un dico. dicotarget = ReadDatabase.get_compte(data["idbde"]) target = Compte(dicotarget) # Maintenant on vérifie qu'on a les droits nécessaires pour changer les champs fournis. # C'est-à-dire : # un champ standard : adherents_weak # nom, prenom, mail ou type : adherents_strong # un droit : le surdroit correspondant # un surdroit : supreme # un mdp : NON ! Il faut utiliser la fonction chgpass # NB : si on modifie son propre compte, on a l'équivalent de adherents_weak + chgpass + wei this_is_my_count = self._myself(data["idbde"]) adh_w, adh_s, supr, chgpass, acl_wei = False, False, False, False, False surd = [] if "type" in fields: if not(data["type"] in ["personne", "club"]): self._send(None, 4, u'type doit être "personne" ou "club".') self._debug(3, u'update_compte failed, mauvais "type"') return # Pour modifier le type d'un compte, il faut les droits adherents_strong adh_s = True # Seuls certains caractères sont autorisés dans les noms/prénoms chartable = u"""abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ""" chartable += u"""àèìòùáéíóúâêîôûäëïöüÿãẽĩõũỹñṽçæøåßÀÈÌÒÙÁÉÍÓÚÂÊÎÔÛÄËÏÖÜYÃẼĨÕŨỸÑṼÇÆØÅ.'-_ 0123456789""" for champ in ["nom", "prenom"]: # champs nécessitant les droits adherents_strong if champ in fields: adh_s = True if not type(data[champ]) in [unicode, str]: self._debug(3, u"update_compte : %s doit être un string." % (champ,)) self._send(None, 4, u"Impossible d'update : %s doit être un string." % (champ,)) return if (data[champ] == ""): self._debug(3, u"update_compte : %s vide." % (champ,)) self._send(None, 4, u"Impossible d'update : %s vide." % (champ,)) return for i in data[champ]: if not i in chartable: self._debug(3, u"update_compte : caractère invalide dans le %s." % (champ,)) self._send(None, 4, u"Caractère invalide dans le %s." % (champ,)) return for champ in ["previous_report_date", "next_report_date"]: if champ in fields: # Ce sont des dates, il faut vérifier qu'elles sont date-formatables. if not( BaseFonctions.isPgsqlDate(data[champ]) ): self._debug(3, u"update_compte : %s ne peut être formaté en date." % (data[champ],)) self._send(None, 4, u"%s ne peut être formaté en date. Abort." % (data[champ],)) return for champ in ["pseudo", "tel", "mail", "adresse", "fonction", "normalien", "pbsante", "report_period", "previous_report_date", "next_report_date", "bloque"]: # champs nécessitant les droits adherents_weak if champ in fields: adh_w = True if (dicotypes[champ] == int) and not(type(data[champ]) == int): self._debug(3, u"update_compte : champ %s, doit être int, pas %s." % (champ, type(data[champ]))) self._send(None, 4, u"Le champ %s, doit être int, pas %s." % (champ, type(data[champ]))) return if (dicotypes[champ] == unicode) and not(type(data[champ]) in [str, unicode]): self._debug(3, u"update_compte : champ %s, doit être unicode, pas %s." % (champ, type(data[champ]))) self._send(None, 4, u"Le champ %s, doit être unicode, pas %s." % (champ, type(data[champ]))) return # La report_period ne doit pas être supérieure à 365 if data.get("report_period", -1) > 365: self._debug(3, u"update_compte : champ report_period supérieur à 365 (%d)." % (data[champ],)) self._send(None, 4, u"Le champ report_period doit être inférieur à 365 (%d)." % (data[champ])) return # Le mail aussi nécessite adherents_strong ou myself if "mail" in fields and not this_is_my_count: adh_s = True if "pseudo" in fields: # Il va falloir vérifier qu'il n'est pas déjà pris if not(BaseFonctions.pseudo_libre(data["pseudo"], data["idbde"])): self._debug(3, u"update_compte : pseudo déjà pris (pseudo/alias/historique). Abort.") self._send(None, 12, u"Pseudo déjà pris ou changé trop récemment ou utilisé comme alias.") return if "passwd" in fields: # update passwd nécessite chgpass self._debug(3, u"update_compte : tentative d'update de passwd. Abort.") self._send(None, 4, u"Pour modifier un mot de passe, utilise chgpass.") return if "pbsante" in fields: acl_wei = True deldroits = False if "droits" in fields: # Il faut penser que si on lui a modifié ses droits et qu'il est en ligne, il les perd # Aucune inquiétude, quand il en aura besoin, ils seront régénérés deldroits = True if not(type(data["droits"]) in [unicode, str]): self._debug(3, u"update_compte : champ droits doit être unicode, pas %s. Abort." % ( type(data["droits"]))) self._send(None, 4, u"Le champ droits doit être unicode, pas %s." % (type(data["droits"]))) return # Il faut avoir les surdroits nécessaires pour les droits ajoutés, mais aussi pour ceux enlevés droits_avant = set(target.get_droits()) droits_apres = set(data["droits"].split(",")) modified = droits_avant.symmetric_difference(droits_apres) surd = list(modified) delsurdroits = False if "surdroits" in fields: # Il faut penser que si on lui a modifié ses droits et qu'il est en ligne, il les perd # Aucune inquiétude, quand il en aura besoin, ils seront régénérés delsurdroits = True if not(type(data["surdroits"]) in [unicode, str]): self._debug(3, u"update_compte : champ surdroits doit être unicode, pas %s. Abort." % ( type(data["surdroits"]),)) self._send(None, 4, u"Le champ surdroits doit être unicode, pas %s." % (type(data["surdroits"]),)) return # Il faudra vérifier qu'on a les droits supreme pour pouvoir modifier des surdroits supr = True # Maintenant on fait la vérification effective des droits if (self.userid == "special"): ok = ( (not(adh_w) or this_is_my_count or self._has_acl("adherents_weak")) and (not(adh_s) or self._has_acl("adherents_strong")) and (not(supr) or self._has_acl("supreme")) and (not(acl_wei) or this_is_my_count or self._has_acl("wei_admin")) and ((surd == []) or self._has_acl("surdroits"))) if not ok: _pasledroit(self, "update_compte") return else: ok = True if adh_w and not(this_is_my_count or self._has_acl("adherents_weak") or self._has_acl("adherents_weak", surdroit=True)): _pasledroit(self, "adherents_weak") return if adh_s and not(self._has_acl("adherents_strong")): _pasledroit(self, "adherents_strong") return if supr and not(self._has_acl("supreme")): _pasledroit(self, "supreme") return if acl_wei and not(this_is_my_count or self._has_acl("wei_admin")): _pasledroit(self, "wei_admin") if (surd != []): surdroits = {i: self._has_acl(i, surdroit=True) for i in surd} if not(all( surdroits.values() )): _pasledroit(self, ", ".join(["surdroit " + k for (k, v) in surdroits.items() if not v])) return if ok: # Quelques légers détails pour les clubs : # ça ne sert à rien de vouloir modifier leur section if (target.type == "club"): if "section" in fields: fields.remove("section") del data["section"] con, cur = BaseFonctions.getcursor() if "pseudo" in fields: # Il faut songer à faire les modifications requises dans l'historique Consistency.add_historique_pseudo(data["idbde"], data["pseudo"], cur) target.modify(data) # on a eu besoin de ce tour de passe-passe parce qu'il faut que le dico # avec lequel on appelle .update soit complet # on fait l'update (méhode de la classe BaseFonctions.Compte) en faisant gaffe que la BDD peut râler try: target.save(cur) # bien entendu il faut faire la suppression après sinon toute demande _has_acl fout tout en l'air if deldroits: self.auth.del_droits_connus(data["idbde"]) if delsurdroits: self.auth.del_droits_connus(data["idbde"]) # pour updater la section, c'est une autre histoire if "section" in fields: cur.execute("UPDATE adhesions SET section = %s WHERE id = (SELECT last_adhesion FROM comptes WHERE idbde = %s);", (data["section"], data["idbde"])) except Exception as exc: self._debug(3, u"update_compte failed : erreur niveau BDD : %s" % (str(exc).decode("utf-8"),)) self._send(None, 555, u"Échec dans la Base de Données : %s" % (str(exc).decode("utf-8"),)) else: self._log("update_compte", cur, data, [data["idbde"]]) cur.execute("COMMIT;") self._debug(1, u"%s updated." % (data["idbde"],)) self._send(u"Compte modifié.") else: _pasledroit(self, "update_compte")
[docs]def update_photo(self, data): """``data = [<idbde>, <la photo base64-encodée>, <format de la photo>]`` Range la photo dans le répertoire des photos après resize/conversion. Insulte si le format de photo n'est pas pris en charge ou si sa taille est trop grande. """ if not((type(data) == list) and ([type(i) for i in data] == [int, unicode, unicode])): _badparam(self, u"update_photo") return idbde, photo, format = data if self._has_acl("update_photo") or self._myself(idbde): format = format.lower().strip(".") if not format in config.photo_allowed_formats: self._debug(3, u"update_photo failed : format %s non autorisé." % (format,)) self._send(None, 802, u"Format de photo non reconnu : %s (Format autorisés : %s)" % (format, u", ".join(config.photo_allowed_formats))) return try: photo = base64.b64decode(photo) except Exception as exc: self._debug(3, u"update_photo failed : échec de b64-décodage") self._send(None, 803, u"Échec de b64-décodage : %s : %s" % (type(exc), exc)) return taille = len(photo) if (taille > config.photo_max_size): self._debug(3, u"update_photo failed : fichier trop gros : %s (max_size : %s)" % (taille, config.photo_max_size)) self._send(None, 801, u"Fichier trop volumineux. Taille maximale = %sK" % (int(config.photo_max_size/1024.0))) return filepath = "%s%s.%s" % (config.photosdir, idbde, format) f = open(filepath, "w") f.write(photo) f.close() if (format != "png"): # On veut conserver toutes les photos en png filepng = "%s%s.png" % (config.photosdir, idbde) cmd = ["convert", filepath, filepng] result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() if (result[1] != ""): # le message d'erreur contient le path, on a envie de le masquer errmsg = result[1].decode("utf-8").replace(config.photosdir, "<notekfet2015_photos_path>/") self._debug(3, u"update_photo erreur à la conversion en .png : %s" % (errmsg,)) self._send(None, 805, u"Erreur à la conversion en .png : %s" % (errmsg,)) return # On supprime donc la photo de l'ancien format try: os.remove(filepath) except Exception as exc: # le message d'erreur contient le path, on a envie de le masquer errmsg = (str(type(exc)) + str(exc)).decode("utf-8").replace(config.photosdir, "<notekfet2015_photos_path>/") self._debug(3, u"update_photo erreur à la suppression du fichier .%s : %s" % (format, errmsg)) self._send(None, 806, u"Erreur à la suppression du fichier .%s : %s" % (format, errmsg)) return filepath = filepng # On va resize la photo pour qu'elle tienne dans 160x160 px cmd = ["convert", filepath, "-resize", "160x160", filepath] result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() if (result[1] != ""): # le message d'erreur contient le path, on a envie de le masquer errmsg = result[1].decode("utf-8").replace(config.photosdir, "<notekfet2015_photos_path>/") self._debug(3, u"update_photo erreur au resize : %s" % (errmsg,)) self._send(None, 804, u"Erreur au resize : %s" % (errmsg,)) return self._send(u"Photo modifiée.") self._debug(1, u"Photo de %s updated." % idbde) else: _pasledroit(self, "update_photo")
[docs]def get_last_modified_photo(self, data): """``data = <idbde>`` Transmet la date (timestamp unix) de dernière modification de la photo n°``<idbde>``. """ if not(type(data) == int): _badparam(self, u"get_last_modified_photo") return idbde = data if self._has_acl("get_photo") or self._myself(idbde): try: timestamp = os.path.getmtime("%s%s.png" % (config.photosdir, idbde)) except: self._debug(4, u"get_last_modified_photo : %s n'a pas de photo" % (idbde,)) self._send(None, 404, u"Photo inexistante.") return self._debug(4, u"date de dernière modification de la photo %s envoyée" % (idbde,)) self._send(timestamp) else: _pasledroit(self, "get_photo")
[docs]def get_photo(self, data): """Transmet une photo, en base64.""" if not (type(data) == int): _badparam(self, u"get_photo") return idbde = data if self._has_acl("get_photo") or self._myself(idbde): # on va chercher la photo try: photo = open("%s%s.png" % (config.photosdir, idbde)).read() except: self._debug(3, u"get_photo failed : %s n'a pas de photo" % (idbde,)) self._send(None, 404, u"Photo inexistante.") return photob64 = base64.b64encode(photo) self._send(photob64) self._debug(4, u"photo %s envoyée" % (idbde,)) else: _pasledroit(self, "update_photo")
[docs]def whoami(self): """Transmet la totalité des données de l'utilisateur courant (sauf la photo).""" if (self.userid == "special"): (passwddb, droits) = self.auth.get_logins()[self.username] self._debug(4, u"Affichage de l'utilisateur spécial %s" % self.username) self._send({"username": self.username, "droits": droits}) else: # isAdherent donne la section, ainsi la section apparaîtra dans l'affichage de l'adhérent compte = Compte(ReadDatabase.get_compte(self.userid), BaseFonctions.isAdherent(self.userid)) compte_data = compte.get_data(True) # True, parce qu'on a le droit de voir ses propres pbsante if self._myself(): self._debug(4, u"Affichage du compte %s (himself)" % self.userid) self._send(compte_data) else: # On ne jette pas un mec qui n'a pas myself, il a besoin d'un minimum vital self._debug(4, u"Affichage du compte %s (himself) mais sans droits myself" % self.userid) self._send({k:v for (k,v) in compte_data.iteritems() if k in config.minimum_account_data})
[docs]def compte(self, data): """Transmet la totalité des informations du compte demandé (sauf la photo).""" if not(type(data) == int): _badparam(self, u"compte") return idbde = data this_is_my_count = self._myself(idbde) if this_is_my_count or self._has_acl("adherents_weak") or self._has_acl("adherents_weak", surdroit=True): try: compte = ReadDatabase.get_compte(idbde) except ExceptionsNote.Error404 as exc: self._send(None, 404, u"Affichage échoué : %s" % (str(exc).decode("utf-8"),)) self._debug(3, u"Affichage de %s échoué. Idbde Unknown." % (idbde,)) return # isAdherent donne la section, ainsi la section apparaîtra dans l'affichage de l'adhérent compte = Compte(compte, BaseFonctions.isAdherent(idbde)) self._debug(4, u"Affichage du compte %s" % (idbde)) self._send(compte.get_data(this_is_my_count or self._has_acl("wei_admin"))) elif not this_is_my_count and self.userid == idbde: _pasledroit(self, "myself") else: _pasledroit(self, "adherents_weak")
[docs]def preinscrire(self, dico): """Enregistre une préinscription. Les données sont envoyées sous forme d'un dico ``{"champ": <valeur>}`` Vérifie que les champs indispensables (``"nom"``, ``"prenom"`` et ``"mail"``) sont corrects. _log relevant ids : preid """ dicotypes = {"type": unicode, "nom": unicode, "prenom": unicode, "tel": unicode, "mail": unicode, "adresse": unicode, "pbsante": unicode, "section": unicode, "normalien": bool} if not((type(dico) == dict) and set(dico.keys()).issubset(dicotypes.keys()) # champs autorisés and set(["nom", "prenom", "mail"]).issubset(dico.keys()) ): # champs obligatoires _badparam(self, u"preinscrire (champs)") return typeok = True for champ in dico.keys(): if (champ == "type"): if not(dico[champ] in ["personne", "club"]): typeok = False else: if (type(dico[champ]) != dicotypes[champ]): typeok = False if not typeok: _badparam(self, u"preinscrire (types)") return if self._has_acl("preinscriptions"): champs_fournis = dico.keys() if "preid" in champs_fournis: self._debug(3, u"Préinscription échouée : preid renseigné.") self._send(None, 4, u"preid ne peut pas être imposé. Ignoré.") return if (dico["nom"] == "") or (dico["prenom"] == ""): self._debug(3, u"Préinscription échouée : nom ou prenom vide.") self._send(None, 4, u"nom et prenom ne peuvent pas êtres vides !") return elif (re.match(ur'[^@]+@.+\..+', dico["mail"]) == None): # re.match renvoie None s'il n'y a pas de match self._debug(3, u"Préinscription échouée : mail invalide") self._send(None, 4, u"mail invalide.") return else: # Bon, là c'est bien parti. On commence par traiter la casse de nom et prénom dico["prenom"], dico["nom"] = dico["prenom"].title(), dico["nom"].title() # On mémorise ce qu'on va logger avant d'ajouter tous les paramètres par défaut con, cur = BaseFonctions.getcursor() dicolog = {k : v for (k, v) in dico.iteritems()} # ensuite on set les default values pour les champs non fournis # (oui ces valeurs par défaut existent dans la base, mais si elles sont fournies il faut bien les # transmettre, donc dans tous les cas il faut que la valeur existe au moment # de la création de la requête) dico_default ={"type": "personne", "tel": "", "adresse": "", "normalien": False, "pbsante": "", "section": ""} for i in dico_default: dico[i] = dico.get(i, False) or dico_default[i] # dico_default[i] est utilisé # si dico[i] n'existait pas # et on exécute cur.execute("""INSERT INTO preinscriptions (type, nom, prenom, tel, mail, adresse, normalien, pbsante, section) VALUES (%(type)s, %(nom)s, %(prenom)s, %(tel)s, %(mail)s, %(adresse)s, %(normalien)s, %(pbsante)s, %(section)s) RETURNING preid; """, dico) preid = cur.fetchone()["preid"] self._log("preinscrire", cur, dicolog, [preid]) cur.execute("COMMIT;") self._debug(1, u"Nouvelle préinscription enregistrée %s" % (dico,)) self._send(u"Préinscription enregistrée.") else: _pasledroit(self, "preinscrire")
[docs]def get_preinscription(self, data): """``data = <preid>`` Transmet les informations d'une préinscription. """ if not (type(data) == int) and (data > 0): _badparam(self, u"get_preinscription") return preid = data if self._has_acl("inscriptions"): con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM preinscriptions WHERE preid = %s;", (preid,)) preinscription = cur.fetchone() if preinscription is None: self._send(None, 404, u"Cette préinscription n'existe pas.") self._debug(3, u"get_preinscription failed : La préinscription %s n'existe pas." % (preid,)) else: self._send(dict(preinscription)) self._debug(4, u"Envoi de la préinscription %s" % (preid,)) else: _pasledroit(self, "inscriptions")
[docs]def get_preinscriptions(self): """Transmet la liste des préinscriptions.""" if self._has_acl("inscriptions"): con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM preinscriptions ORDER BY nom, prenom;") preinscriptions = cur.fetchall() self._send(preinscriptions) self._debug(4, u"Envoi de la liste des préinscriptions.") else: _pasledroit(self, "inscriptions")
[docs]def del_preinscription(self, data): """``data = <preid>`` Supprime une préinscription. _log relevant ids : preid """ if not(type(data) == int and data > 0): _badparam(self, u"del_preinscription") return preid = data if self._has_acl("inscriptions"): con, cur = BaseFonctions.getcursor() cur.execute("DELETE FROM preinscriptions WHERE preid = %s;", (preid,)) self._log("del_preinscription", cur, preid, [preid]) cur.execute("COMMIT;") self._debug(1, u"del_preinscription %s : success" % (preid,)) self._send("Préinscription supprimée.") else: _pasledroit(self, "inscription")
[docs]def get_default_pseudo(self, data): """``data = [<nom>, <prénom>]`` Transmet le résultat de la génération du pseudo pour ce couple (nom, prénom). """ if not((type(data) == list) and ([type(i) for i in data] == [unicode] * 2)): _badparam(self, u"get_default_pseudo") return nom, prenom = data if self._has_acl("inscriptions"): pseudo = BaseFonctions.default_pseudo(nom, prenom) self._send(pseudo) self._debug(4, u"Envoi du pseudo par défaut pour le couple (nom, prénom) = (%s, %s)" % (nom, prenom)) else: _pasledroit(self, "inscriptions")
[docs]def _private_adhesion(self, idbde, annee, debit, section, wei=False, cur=None): """Adhère ``idbde`` pour l'année ``annee``. Enregistre la transaction ``"Adhésion"`` et l'adhésion. Update le solde sans faire de vérification. (crédite la note 0). Retourne l'id de la transaction d'adhésion et celui de l'adhésion elle-même. Si on l'appelle avec ``cur=<un_curseur>``, ce curseur est utilisé et n'est pas COMMITé à la fin. """ if cur == None: con, cur = BaseFonctions.getcursor() cur_given = False else: cur_given = True # on le fait payer (attention, on ne fait pas de vérification sur le solde !) _, idtransaction = _une_transaction(self, u'adhésion', idbde, 0, 1, debit, "Adhésion %s%s" % (annee, " + WEI" if wei else "",), 'Adhésion', doitanyway=True, cantinvalidate=True, cur=cur) # On l'adhère cur.execute("""INSERT INTO adhesions (idbde, annee, wei, idtransaction, section) VALUES (%s, %s, %s, %s, %s) RETURNING id;""", (idbde, annee, wei, idtransaction, section)) # On récupère l'id de l'adhésion pour le mettre dans la table comptes idadhesion = cur.fetchone()["id"] cur.execute("UPDATE comptes SET last_adhesion = %s WHERE idbde = %s;", (idadhesion, idbde)) if not cur_given: # On COMMIT. _une_transaction ne l'a pas encore fait, donc on assure la cohérence cur.execute("COMMIT;") return [idtransaction, idadhesion]
[docs]def inscrire(self, data): """ Deux possibilités : * ``data = [<ident>, <dico>, <pay>, <from_wei>]`` * ``data = [<ident>, <dico>, <pay>, <from_wei>, <override_adh>]`` Si <from_wei> est faux, <ident> est une <preid> * Valide une préinscription. ** ``<preid>`` est l'identifiant de préinscription, ** ``<dico>`` contient des tas d'infos (``"wei"`` et ``"annee"`` étant obligatoires) ** ``<pay> = [<on_note>, <type_de_paiement>, <params>]``, avec : ** ``<on_note>`` = solde à ajouter à la note (en centimes) ** ``<type_de_paiement>`` à valeur dans ``"cheque", "especes", "virement", "soge"`` ** ``params = {"nom": <nom>, "prenom": <prenom>, "banque": <banque>}`` ** ``<overripde_adh>`` est le montant qu'on veut que l'adhérent paye pour son inscription (facultatif, nécessite le droit ``"adhesions_admin"`` si différent du montant par défaut) Sinon <ident> est un <idwei> : ** <idwei> est l'identifiant du wei ** <dico> contient des tas d'infos ** ``<pay> = [<on_note>, <type_de_paiement>, <params>]``, avec : plus tard .... _log relevant ids : preid, idbde, id de la transaction de crédit (ou NULL), id de moyen de paiement (ou NULL), id de la transaction d'adhésion (ou NULL), id de l'adhésion (ou NULL) """ dicotypes = {"type": unicode, "nom": unicode, "prenom": unicode, "mail": unicode, "pseudo": unicode, "passwd": unicode, "normalien": unicode, "fonction": unicode, "pbsante": unicode, "report_period": int, "tel": unicode, "adresse": unicode, "section": unicode, "annee": int, "normalien": bool, "wei": bool, "commentaire": unicode, "bloque": bool} dates = ["previous_report_date", "next_report_date"] # les dates sont un cas un peu particulier # On mémorise la version fournie pour pouvoir faire joujou avec avec mais loguer correctement datasaved = copy.deepcopy(data) if (type(data) == list) and ([type(i) for i in data] in [[int, dict, list, bool], [int, dict, list, bool, int]]): if len(data) == 4: ident, dico, pay, from_wei = data override_adh = None elif len(data) == 5: ident, dico, pay, from_wei, override_adh = data if override_adh < 0: _badparam(self, u"inscrire (override montant <0)") return else: _badparam(self, u"inscrire (type)") return if not (set(dico.keys()).issubset(dicotypes.keys() + dates) # Champs possibles and set(["wei"]).issubset(dico.keys()) ): # Champs obligatoires _badparam(self, u"inscrire (clés)") return # Si l'année n'est pas fournie on prend par défaut l'année en cours dico["annee"] = BaseFonctions.adhesion_current_year() # On vérifie pas pay si on inscrit un club if dico.has_key("type") and (dico["type"] == "club"): thisisaclub = True else: thisisaclub = False if not (([type(i) for i in pay] == [int, unicode, dict]) and pay[1] in ["cheque", "especes", "cb", "virement", "soge"]): _badparam(self, u"inscrire (paiement)") return # on vérifie que params de pay est correct seulement si on paie par cheque ou virement if pay[1] in ["cheque", "virement", "cb"]: if not(set(["nom", "prenom", "banque"]).issubset(pay[2].keys())): _badparam(self, u"inscrire (params paiment)") return if "" in [pay[2][k] for k in ["nom", "prenom", "banque"]]: self._debug(3, u"inscrire : nom ou prenom ou banque non spécifié pour le paiement") self._send(None, 4, u"Nom, Prénom et Banque doivent être spécifiés pour le paiement.") return typeok = True for champ in dico.keys(): if champ in dates: if not BaseFonctions.isPgsqlDate(dico[champ]): typeok, reason = False, u"dates" elif (champ == "type"): if not dico[champ] in ["personne", "club"]: typeok, reason = False, u"type" else: if (type(dico[champ]) != dicotypes[champ]): typeok, reason = False, u"typage du champ %s" % champ if not typeok: _badparam(self, u"inscrire (%s)" % reason) return if self._has_acl("inscriptions") or self._has_acl("wei_admin"): # On initialise la connexion à la base de données con, cur = BaseFonctions.getcursor() # Si on est face a une inscription "normale" if not from_wei: # On commence par récupérer les données de la préinscription preid = ident cur.execute("SELECT * FROM preinscriptions WHERE preid = %s;", (preid,)) # Si on est face a une inscription wei else: idwei = ident cur.execute("SELECT *, dept AS section, infos AS pbsante FROM wei_1a WHERE idwei = %s;", (idwei,)) l = cur.fetchall() if (len(l) != 1): self._debug(3, u"Tentative d'inscription d'un element inexistant.") self._send(None, 404, u"Cet élément n'existe pas.") else: pre_dico = dict(l[0]) # Si on vient du wei, on le signale if from_wei: pre_dico["wei"] = True # on vérifie qu'on a la section if (pre_dico["section"] == '') and not("section" in dico.keys()): self._debug(3, u"inscrire : section non spécifiée (à la préinscription non plus)") self._send(None, 4, u"La section n'a pas été spécifiée à la préinscription, tu dois la spécifier.") return pre_dico.update(dico) # du coup il contient des trucs en trop, mais on s'en fout if pre_dico.has_key("pseudo"): # On vérifie que le pseudo demandé n'est pas déjà pris if not BaseFonctions.pseudo_libre(pre_dico["pseudo"]): self._debug(3, u"inscrire : pseudo déjà pris (pseudo/alias/historique). Abort.") self._send(None, 12, u"Pseudo déjà pris ou changé trop récemment ou utilisé comme alias.") return else: # Si rien n'est proposé, il faut lui créer son pseudo par défaut pre_dico["pseudo"] = BaseFonctions.default_pseudo(pre_dico["nom"], pre_dico["prenom"]) # Et lui générer un mot de passe real_passwd, hashedpass = BaseFonctions.random_chain(7, 8) # Le clair, on lui enverra par mail dès qu'on connaîtra son idbde # Le hashé, on le mettra dans la base pre_dico["passwd"] = hashedpass # D'abord on lui crée son compte adhérent, # avec un certain nombre de paramètres par défaut on_note, mode, params_pay = pay pre_dico["solde"] = on_note tomorrow = unicode(time.strftime("%Y-%m-%d", time.localtime(time.mktime(time.localtime()) + 3600*24))) default_dico = {"fonction": u"", "report_period":-1, "next_report_date": tomorrow, "bloque": False, "commentaire": u""} for (k,v) in default_dico.items(): pre_dico.setdefault(k ,v) # On uilise "RETURNING idbde" pour récupérer l'idbde que la base a généré cur.execute("""INSERT INTO comptes (type, pseudo, passwd, solde, nom, prenom, tel, mail, adresse, fonction, normalien, pbsante, report_period, next_report_date, bloque, commentaire) VALUES (%(type)s, %(pseudo)s, %(passwd)s, 0, %(nom)s, %(prenom)s, %(tel)s, %(mail)s, %(adresse)s, %(fonction)s, %(normalien)s, %(pbsante)s, %(report_period)s, %(next_report_date)s, %(bloque)s, %(commentaire)s) RETURNING idbde;""", pre_dico) pre_dico["idbde"] = cur.fetchone()["idbde"] log_ids = [ident, pre_dico["idbde"]] # On ne fait pas payer l'adhésion à un club if thisisaclub: log_ids += [None, None, None, None] else: # On va chercher les prix des adhésions de période de WEI cur.execute("""SELECT prix_wei_normalien, prix_wei_non_normalien FROM configurations WHERE used;""") prix_wei_normalien, prix_wei_non_normalien = cur.fetchone() if (pre_dico["wei"] == True): if (pre_dico["soge"] == True): pre_dico["debit"] = 0 else: pre_dico["debit"] = (prix_wei_normalien if (pre_dico["normalien"] == True) else prix_wei_non_normalien) else: pre_dico["debit"] = __price_today(self) # On a le droit de changer le montant de l'adhésion si on les droits pour if override_adh is not None and override_adh != pre_dico["debit"]: if self._has_acl("adhesions_admin"): pre_dico["debit"] = override_adh else: # Ça peut paraître perturbant de quitter ici alors qu'on a déjà fait un INSERT # mais no worries, le curseur n'est pas commité, donc on n'a rien cassé. _pasledroit(self, "adhesions_admin") return # On va lui créditer le montant de l'inscription (qui sera débité juste après) + ce qu'il met sur sa note pre_dico["credit"] = pre_dico["debit"] + on_note # On a envie que "idbde" et "credit" soient accessibles dans le dico params_pay params_pay["idbde"] = pre_dico["idbde"] params_pay["credit"] = pre_dico["credit"] # On lui fait son crédit si il y a vraiment quelque chose à créditer if pre_dico["credit"] > 0: result, sublog_ids = _un_credit(self, mode, pre_dico["idbde"], pre_dico["credit"], u"crédit d'adhésion", params_pay, doitanyway=True, cur=cur) log_ids += sublog_ids else: log_ids += [None, None] # On l'adhère ids_credit_et_adh = _private_adhesion(self, pre_dico["idbde"], pre_dico["annee"], pre_dico["debit"], pre_dico["section"], pre_dico["wei"], cur=cur) log_ids += ids_credit_et_adh # Et enfin on supprime la préinscription si on n'est pas dans le cas du wei if not from_wei: cur.execute("DELETE FROM preinscriptions WHERE preid = %(preid)s;", pre_dico) else: cur.execute("UPDATE wei_1a SET adhere = TRUE WHERE idwei = %s;", (idwei,)) self._log("inscrire", cur, datasaved, log_ids) # On COMMIT à la fin, comme ça si on a crashé au milieu, no problem # NB : les fonctions appelées en dessous se servent du même curseur et ne le commitent pas cur.execute("COMMIT;") self._debug(1, u"Inscription du n°%s" % (pre_dico["idbde"],)) # Maintenant qu'on a l'idbde on peut envoyer le mail mail.mail_inscription(pre_dico, real_passwd) self._send(u"Inscription validée.") else: _pasledroit(self, "inscrire")
[docs]def __in_cheap_period(self): """Renvoie le booléen correspondant à si on est dans la période pendant laquelle l'adhesion est moins chère""" con,cur = BaseFonctions.getcursor() m, d = time.localtime()[1:3] cur.execute("SELECT start_cheaper_adh_month, start_cheaper_adh_day, start_next_year_month, start_next_year_day FROM configurations WHERE used;") dates = cur.fetchone() return( (m,d) >= (dates["start_cheaper_adh_month"],dates["start_cheaper_adh_day"]) and (m,d) < (dates["start_next_year_month"],dates["start_next_year_day"]) )
[docs]def __price_today(self): """Renvoie le prix de l'adhésion correspondant à la période de l'année actuelle""" con,cur = BaseFonctions.getcursor() cur.execute("SELECT prix_adhesion,prix_adhesion_late FROM configurations WHERE used;") prix_adhesion,prix_adhesion_late = cur.fetchone() if __in_cheap_period(self): return prix_adhesion_late else: return prix_adhesion
[docs]def get_tarifs_adhesion(self): """Envoie les prix des adhésions avec ou sans WEI.""" if __in_cheap_period(self): # Si on est dans la période "pas chère", on envoie le petit prix en le faisant passer pour le prix (parce que c'est bien plus simple) string = " prix_adhesion_late AS" else : # Sinon on envoie le prix habituel string = "" # Et on envoie la requête con, cur = BaseFonctions.getcursor() cur.execute("SELECT prix_wei_normalien, prix_wei_non_normalien,%s prix_adhesion FROM configurations WHERE used;" % (string)) tarifs = cur.fetchone() self._debug(4, u"demande des tarifs d'adhésion") self._send(dict(tarifs))
[docs]def readherer(self, data): """Effectue une réadhésion. ``data`` = un dico * Clés obligatoires : ``"idbde"``, ``"section"``. * Clé facultative : ``"wei"``, ``"pay"`` * ``pay`` = un dico. * Clé obligatoire : ``"type"`` (``"especes"``, ``"cheque"``, ``"virement"``) * Clés conditionnelles : ``"prenom"``, ``"nom"``, ``"banque"`` (à fournir si type est ``"cheque"`` ou ``"virement"``) * Clés facutatives : ``"montant"`` (par défaut, vaudra le montant de l'adhésion) _log relevant ids : idbde, id de la transaction de crédit pré-adhésion (ou NULL), id du moyen de paiement (ou NULL), id de la transaction d'adhésion, id de l'adhésion """ if not ((type(data) == dict) and set(["idbde", "section"]).issubset(data.keys())): _badparam(self, u"readherer") return idbde, section = data["idbde"], data["section"] log_ids = [idbde] if idbde <= 0: self._debug(3, u"réadhésion de %s failed : idbde < 0." % (idbde,)) self._send(None, 15, u"Ce compte ne peut pas adhérer.") return # Par défaut, on ne va pas au WEI wei = data.get("wei", False) annee = BaseFonctions.adhesion_current_year() # On vérifie que c'est pas déjà fait con, cur = BaseFonctions.getcursor() cur.execute("SELECT count(*) AS nb FROM adhesions WHERE idbde=%s AND annee=%s;", (idbde, annee)) nb = cur.fetchone()["nb"] if nb == 0: # On va chercher les prix des adhésions ainsi que les périodes par prix cur.execute("""SELECT prix_wei_normalien, prix_wei_non_normalien FROM configurations WHERE used;""") prix_wei_normalien, prix_wei_non_normalien = cur.fetchone() cur.execute("SELECT normalien FROM comptes WHERE idbde=%s;", (idbde,)) normalien = cur.fetchone()["normalien"] if wei: if normalien: debit = prix_wei_normalien else: debit = prix_wei_non_normalien else: debit = __price_today(self) if data.has_key("pay"): # Le client a demandé à faire un crédit avant la réadhésion pay = data["pay"] destinataire, montant, commentaire = idbde, pay.get("montant", debit), pay.get("commentaire", u"pré-adhésion") if not pay.has_key("type"): _badparam(self, u"readherer (pay: pas de clé type)") return pay_type = pay["type"] if not pay_type in ["especes", "cheque", "virement", "cb", "soge"]: _badparam(self, u"readherer (pay: type de paiement inconnu)") return if pay_type in ["cheque", "virement"] and (pay.get("nom", "") == "" or pay.get("prenom", "") == "" or pay.get("banque", "") == ""): _badparam(self, u"readherer (pay: préciser nom, prenom, banque pour un crédit de ce type)" ) return result, sublog_ids = _un_credit(self, pay_type, destinataire, montant, commentaire, params_pay=pay, doitanyway=True, cur=cur) log_ids += sublog_ids else: log_ids += [None, None] ids_credit_et_adh = _private_adhesion(self, idbde, annee, debit, section, wei, cur=cur) log_ids += ids_credit_et_adh self._log("readherer", cur, data, log_ids) cur.execute("COMMIT;") self._debug(1, u"réadhésion de %s pour %s" % (idbde, annee)) self._send(u"Adhésion de %s pour l'année %s." % (idbde, annee)) else: self._debug(3, u"réadhésion de %s pour %s failed : déjà %s adhésion pour ces données." % (idbde, annee, nb)) self._send(None, 14, u"%s a déjà adhéré pour l'année %s." % (idbde, annee))
[docs]def supprimer_compte(self, data): """ Place le champ ``deleted`` du compte à ``true``. data = ``<idbde>`` ou ``[<idbde>, True]``. Échoue si le solde du compte est non nul. Si le deuxième paramètre est à True, anonymise le compte. _log relevant ids : idbde """ if isinstance(data, int): idbde, anonymise = data, False elif isinstance(data, list) and len(data) == 2 and isinstance(data[0], int) and data[0] > 0 and isinstance(data[1], bool): [idbde, anonymise] = data else: _badparam(self, u"supprimer_compte") return if self._has_acl("supprimer_compte") and not self.userid == "special": try: compte = Compte(ReadDatabase.get_compte(idbde)) except ExceptionsNote.Error404: self._debug(3, u"supprimer_compte : erreur : %s n'existe pas." % (idbde)) self._send(None, 404, u"Le compte %s n'existe pas." % (idbde)) return if compte.deleted: self._debug(3, u"supprimer_compte : erreur : %s est déjà supprimé." % (idbde)) self._send(None, 412, u"Tu ne peux pas supprimer ce compte, il est déjà supprimé.") return if compte.solde != 0: self._debug(3, u"supprimer_compte : erreur : %s a un solde non nul." % (idbde)) self._send(None, 410, u"Tu ne peux pas supprimer ce compte, car son solde est non nul.") return # On vérifie si le compte à supprimer n'a pas plus de droits que l'utilisateur courant (vite) if not BaseFonctions.hasMoreRights(Compte(ReadDatabase.get_compte(self.userid)), compte): self._debug(3, u"supprimer_compte : erreur : %s a plus de droits que %s." % (idbde, self.userid)) self._send(None, 411, u"Ce compte a trop de droits.") return if anonymise: compte.anonymise() con, cur = BaseFonctions.getcursor() compte.deleted = True compte.save(cur) self._log("supprimer_compte", cur, data, [idbde]) cur.execute("COMMIT;") self._debug(1, u"compte %s supprimé." % (idbde)) self._send(u"Le compte %s a bien été supprimé." % (idbde)) else: _pasledroit(self, "supprimer_compte")
[docs]def get_boutons(self, search_et_categ): """ ``search_et_categ = [<terme>, <categorie>, <flags>]`` Transmet la liste (json) des boutons qui matchent le terme de recherche et qui sont dans la catégorie (si elle est différente de ``""``) et qui ont ``affiche = true`` Le paramètre ``flags`` est optionnel et peut contenir les drapeaux suivants : * ``a`` (all) --> Cherche parmi _tous_ les boutons (même les non affichés) * ``b`` (begin) --> Matche ``terme`` à partir du début du label """ if not ((type(search_et_categ) == list) and ([type(i) for i in search_et_categ] == [unicode] * len(search_et_categ)) and len(search_et_categ) in [2, 3]): _badparam(self, u"get_boutons") return if (len(search_et_categ) == 3) and (u"a" in search_et_categ[-1]): tous = True else: tous = False if (len(search_et_categ) == 3) and (u"b" in search_et_categ[-1]): starts_with = True else: starts_with = False term, categ = search_et_categ[0:2] if self._has_acl("get_boutons"): con, cur = BaseFonctions.getcursor() # where va contenir le squelette de la clause WHERE et tup les données à injecter dedans where = "label ILIKE %s" if starts_with: tup = ['%s%%' % term] else: tup = ['%%%s%%' % term] if (categ != ""): where += " AND categorie = %s" tup.append(categ) if not tous: where += " AND affiche = true" cur.execute("SELECT boutons.*, pseudo AS destinatairepseudo FROM boutons, comptes WHERE boutons.destinataire = comptes.idbde AND " + where + " ORDER BY label;", tup) # oui tup n'est pas un tuple, mais ça passe quand même boutons_liste = cur.fetchall() # les objets ne sont pas JSON-isables car PgSQL-typés. On en fait donc des dicos. boutons_liste = [dict(i) for i in boutons_liste] self._send(boutons_liste) self._debug(4, u"get_boutons avec %s" % (search_et_categ,)) return boutons_liste else: _pasledroit(self, "get_boutons")
[docs]def get_un_bouton(self, data): """``data = <id>`` Transmet les informations sur le bouton n°``<id>``. """ if not((type(data) == int) and (data > 0)): _badparam(self, u"get_un_bouton") return idbouton = data if self._has_acl("get_boutons"): con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM boutons WHERE id = %s;", (idbouton,)) bouton = cur.fetchone() if bouton: self._send(dict(bouton)) self._debug(4, u"envoi du bouton n°%s" % (idbouton,)) else: self._debug(3, u"get_un_bouton failed : isbouton (%s) inconnu" % (idbouton,)) self._send(None, 404, u"Ce bouton n'existe pas.") else: _pasledroit(self, "get_boutons")
[docs]def get_boutons_categories(self, all=False): """Transmet la liste des catégories de boutons existantes.""" if self._has_acl("get_boutons"): con, cur = BaseFonctions.getcursor() if all: cond = " " else: cond = " WHERE affiche " req = "SELECT DISTINCT categorie FROM boutons%sORDER BY categorie;" % cond cur.execute(req) categories = cur.fetchall() self._send([cat["categorie"] for cat in categories]) self._debug(4, u"envoi de la liste des catégories de boutons (all=%s)" % all) else: _pasledroit(self, "get_boutons")
[docs]def get_clubs(self): """Transmet la liste des ``{"idbde : <idbde>, "pseudo": <pseudo>}`` pour tous les clubs""" if self._has_acl("create_bouton"): con, cur = BaseFonctions.getcursor() cur.execute("SELECT idbde, pseudo FROM comptes WHERE type = 'club' ORDER BY pseudo;") l = cur.fetchall() self._send([dict(i) for i in l]) self._debug(4, u"envoi de la liste des clubs") else: _pasledroit(self, "create_bouton")
[docs]def create_bouton(self, data): """ ``data = {"label" : <nom du bouton>, "montant" : <prix en centimes>, "destinataire" : <idbde du compte à créditer>, "categorie" : <nom de la catégorie>, "affiche" : <booléen afficher le bouton ?>, "description" : <description du bouton>, "consigne" : <booléen est-ce une bouteille consignée ?> }`` Champs obligatoires : label, montant, destinataire, categorie Valeur par défaut des champs facultatifs : affiche : ``True``, description : ``""``, consigne : ``False`` Ajoute un bouton. La catégorie doit déjà exister. *(créer des catégories doit se faire en accès direct à la base)* _log relevant ids : idbouton """ champs_obligatoires = ["label", "montant", "destinataire", "categorie"] champs = champs_obligatoires + ["affiche", "description", "consigne"] data.setdefault("affiche", True) data.setdefault("description", u"") data.setdefault("consigne", False) if not((type(data) == dict) and set(champs_obligatoires).issubset(data.keys()) and ([type(data[i]) for i in champs] == [unicode, int, int, unicode, bool, unicode, bool]) and (not 'affiche' in data.keys() or (type(data['affiche']) == bool)) and (data["montant"] >= 0)): _badparam(self, u"create_bouton") return if self._has_acl("create_bouton"): # On cherche quelles sont les catégories existantes, car on n'a pas le # droit de créer un bouton dans une catégorie qui n'existe pas. con, cur = BaseFonctions.getcursor() cur.execute("SELECT id FROM boutons WHERE categorie = %s;", (data["categorie"],)) if len(cur.fetchall()) == 0: self._debug(3, u"create_bouton : catégorie inexistante : %s. Abort." % (data["categorie"],)) self._send(None, 4, u"Cette catégorie n'existe pas.") return # On impose un label de bouton à ne pas commencer ni terminer par une espace if not re.match(u"^(\S.*\S|\S)$", "n"): self._debug(3, u"create_bouton : label de bouton invalide (%r)" % (data["label"])) self._send(None, 4, u"Un label de bouton ne peut pas commencer/terminer par un espace.") # On vérifie que le destinataire est bien un club cur.execute("SELECT * FROM comptes WHERE idbde = %s;", (data["destinataire"],)) desti = cur.fetchone() if desti is None: self._debug(3, u"create_bouton : l'idbde %s n'existe pas. Abort." % (data["destinataire"],)) self._send(None, 4, u"Ce destinataire n'existe pas.") return if (desti["type"] == "club"): # C'est bon try: cur.execute(""" INSERT INTO boutons (label, montant, destinataire, categorie, affiche, description, consigne) VALUES (%(label)s, %(montant)s, %(destinataire)s, %(categorie)s, %(affiche)s, %(description)s, %(consigne)s) RETURNING id; """, data) idbouton = cur.fetchone()["id"] self._log("create_bouton", cur, data, [idbouton]) cur.execute("COMMIT;") self._debug(1, u"create_bouton : success %s" % (data,)) self._send("Bouton créé.") except Exception as e: if str(e).split("\n")[0] == """ERREUR: la valeur d'une clé dupliquée rompt la contrainte unique « pas_deux_fois_le_meme_bouton »""": # On a essayé d'ajouter un bouton déjà existant self._debug(4, u"create_bouton : bouton %s déjà existant." % (data,)) self._send(None, 103, u"Un bouton %s dans la catégorie %s existe déjà : rien n'a été modifié." % (data["label"], data["categorie"])) return else: raise else: self._debug(3, u"create_bouton : le destinataire %s n'est pas un club. Abort." % (data["destinataire"])) self._send(None, 4, u"Le destinataire doit être un club.") return else: _pasledroit(self, "create_bouton")
[docs]def update_bouton(self, data): """ ``data`` = un dictionnaire contenant au moins la clé ``"id"`` et pouvant contenir les clés de ``create_bouton``. Édite le bouton ``data["id"]``. _log relevant ids : idbouton """ dicotypes = {"id": int, "label": unicode, "montant": int, "destinataire": int, "categorie": unicode, "affiche": bool, "description" : unicode, "consigne" : bool} if not((type(data) == dict) and "id" in data.keys() and (not any([(type(data[i]) != dicotypes[i]) for i in data.keys()])) and (not ("montant" in data.keys()) or (data["montant"] >= 0))): _badparam(self, u"update_bouton") return if self._has_acl("update_bouton"): # ça peut arriver con, cur = BaseFonctions.getcursor() # On va d'abord récupérer le bouton existant pour avoir les valeurs non modifiées cur.execute("SELECT * FROM boutons WHERE id = %s;", (data["id"],)) bouton = cur.fetchone() if bouton is None: self._debug(3, u"update_bouton : le bouton %s n'existe pas. Abort." % (data["id"])) self._send(None, 4, u"Ce bouton n'existe pas.") return bouton = dict(bouton) # Il faut vérifier, comme à la création, que destinataire est bien un club, # que categorie existe... if "categorie" in data.keys(): cur.execute("SELECT categorie FROM boutons GROUP BY categorie;") categs = cur.fetchall() categs = [i[0] for i in categs] if not data["categorie"] in categs: self._debug(3, u"update_bouton : catégorie %s inexistante. Abort." % (data["categorie"])) self._send(None, 4, u"Cette catégorie n'existe pas.") return if "destinataire" in data.keys(): cur.execute("SELECT type FROM comptes WHERE idbde = %s;", (data["destinataire"],)) type_compte = cur.fetchone()["type"] if (type_compte != "club"): self._debug(3, u"update_bouton : le n°%s n'est pas un club (ou n'existe pas). Abort." % (data["destinataire"],)) self._send(None, 4, u"Ce destinataire n'est pas un club (ou n'existe pas).") return # Là, c'est bon bouton.update(data) try: cur.execute("""UPDATE boutons SET label = %(label)s, montant = %(montant)s, destinataire = %(destinataire)s, categorie = %(categorie)s, affiche = %(affiche)s, description = %(description)s, consigne = %(consigne)s WHERE id = %(id)s;""", bouton) self._log("update_bouton", cur, data, [data["id"]]) cur.execute("COMMIT;") self._debug(1, u"update_bouton : %s" % (data,)) self._send("Bouton modifié.") return except Exception as e: if (str(e).split("\n")[0] == """ERREUR: la valeur d'une clé dupliquée rompt la contrainte unique « pas_deux_fois_le_meme_bouton »"""): # On a essayé de le changer en un bouton déjà existant self._debug(3, u"update_bouton : bouton cible déjà existant %s" % (data,)) self._send(None, 4, u"Modification impossible : un bouton identique existe déjà.") return else: _pasledroit(self, "update_bouton")
[docs]def delete_bouton(self, ident): """ Supprime le bouton n°``ident``. _log relevant ids : idbouton """ if not(type(ident) == int): _badparam(self, u"delete_bouton") return if self._has_acl("delete_bouton"): con, cur = BaseFonctions.getcursor() cur.execute("DELETE FROM boutons WHERE id = %s;", (ident,)) self._log("delete_bouton", cur, ident, [ident]) cur.execute("COMMIT;") self._debug(1, u"delete_bouton %s : success" % (ident,)) self._send("Bouton supprimé.") else: _pasledroit(self, "delete_bouton")
[docs]def _insert_transaction(self, typ, idemetteur, iddestinataire, qte, montant, description, categorie, valide, cantinvalidate=False, cur=None): """Met une transaction dans la table et update les soldes émetteur et destinataire si ``valide = True``. Ne sert qu'à factoriser du code et ne fait absolument aucune vérification. Renvoie l'id de la transaction. *Si on l'appelle avec ``cur=<un_curseur>``, celui-ci est utilisé et n'est pas COMMITé à la fin.* """ # Comme on ne fait aucune vérification, mais qu'on n'a quand même pas totalement # confiance en l'appelant, on va faire le tout dans un try try: if cur is None: con, cur = BaseFonctions.getcursor() cur_given = False else: cur_given = True # on insère la transaction avec RETURNING id try: cur.execute("""INSERT INTO transactions (type, emetteur, destinataire, quantite, montant, description, categorie, valide, cantinvalidate) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id;""", (typ, idemetteur, iddestinataire, qte, montant, description, categorie, valide, cantinvalidate)) except psycopg2.DataError as exc: _badparam(self, "insert_transaction : %s" % (exc,)) raise ExceptionsNote.WaitNextCommand ident = cur.fetchone()["id"] if valide: to_pay = montant * qte cur.execute("UPDATE comptes SET solde = solde - %s WHERE idbde = %s;", (to_pay, idemetteur)) cur.execute("UPDATE comptes SET solde = solde + %s WHERE idbde = %s;", (to_pay, iddestinataire)) if not cur_given: # On ne COMMIT; qu'à la fin, pour assurer la cohérence même en cas de crash cur.execute("COMMIT;") return ident except ExceptionsNote.WaitNextCommand: # évidemment, il faut laisser remonter cette erreur raise except Exception as exc: trace = " " + traceback.format_exc().replace("\n", "\n ")[:-4] raise ExceptionsNote.TuTeFousDeMaGueule("Tu as appelé _insert_transaction, je te faisais confiance, et regarde ce que je me suis pris :\n" + trace)
[docs]def _une_transaction(self, typ, idemetteur, iddestinataire, qte, montant, description, categorie='', forced=False, overforced=False, doitanyway=False, justtesting=False, cantinvalidate=False, cur=None): """Effectue la transaction demandée. Ne peut pas être appelée par le client. Ne fait pas de vérification de droits. Si l'émetteur a un solde trop faible, et que l'utilisateur courant n'a pas les droits nécessaires, la transaction est enregistrée en ``valide = false``. Ne log rien, ne debug rien (c'est le rôle des fonctions appelantes). Renvoie une paire : * une chaîne de la forme ``"{ok|failed} : {normal|forced|overforced}[ needed]"`` * l'id de la transaction On peut l'appeler avec ``doitanyway`` à ``True`` et alors, pour peu que les comptes existent, on fera la transaction, même si les comptes ne sont pas à jour d'adhésion ou si les droits forced/overforced ne sont pas présents. (Utile pour les adhésions) Si ``justtesting`` est à ``True``, la transaction n'est pas insérée, on répond juste si elle aurait pu être faite. *Si on l'appelle avec ``cur=<un_curseur>``, il est utilisé et n'est pas COMMITé à la fin.* """ # Le typ est limité if not(typ in config.types_transactions): raise ExceptionsNote.TuTeFousDeMaGueule("_une_transaction : type non autorisé (%r)" % (typ,)) # On ne fait pas de transactions de montant ou qté < 0 if (montant < 0): raise ExceptionsNote.TuTeFousDeMaGueule("_une_transaction : montant <0 (%s)" % (montant,)) if (qte < 0): raise ExceptionsNote.TuTeFousDeMaGueule("_une_transaction : quantité <0 (%s)" % (qte,)) if cur is None: con, cur = BaseFonctions.getcursor() cur_given = False else: cur_given = True # On va chercher l'emetteur et le destinataire store = [] for (ident, nom) in [(idemetteur, "Émetteur"), (iddestinataire, "Destinataire")]: cur.execute("SELECT * FROM comptes WHERE idbde = %s", (ident,)) compte = cur.fetchone() if compte is None: raise ExceptionsNote.Error404("%s inexistant : %s" % (nom, ident,)) if compte["bloque"] or compte["deleted"]: raise ExceptionsNote.AccountBlocked(ident) store.append(compte) [emetteur, destinataire] = store # On va chercher dans la config ce que signifie "être en négatif" cur.execute("""SELECT solde_tres_negatif, solde_pas_plus_negatif FROM configurations WHERE used = true;""") solde_tres_negatif, solde_pas_plus_negatif = cur.fetchone() to_pay = montant * qte solde_emetteur_apres = emetteur["solde"] - to_pay solde_emetteur_avant = emetteur["solde"] # On vérifie qu'emetteur et destinataire sont à jour d'adhésion # sauf si dontcareuptodate (pour les adhésions) emetteur_ajour = BaseFonctions.isAdherent(idemetteur)[0] destinataire_ajour = BaseFonctions.isAdherent(iddestinataire)[0] if (emetteur_ajour and destinataire_ajour) or doitanyway or self._has_acl("transactions_admin"): ok = True else: ok = False # mais tout n'est pas perdu ! # L'émetteur ou le destinataire n'est pas à jour d'adhésion # Les seules transactions alors autorisées sont les suivantes : # from | to | type | solde_emetteur_apres | solde_destinataire_apres # BdE | * | crédit | * | <= 0 # à jour | * | transfert ou don | obéit aux forced | <= 0 # * | BdE | retrait ou transfert | >= 0 | * solde_destinataire_apres = destinataire["solde"] + to_pay if (idemetteur == 0) and (typ == "crédit") and (solde_destinataire_apres <= 0): ok = True elif emetteur_ajour and typ in ["transfert", "don"] and (solde_destinataire_apres <= 0): # pour la part "solde_emetteur_apres obéit aux forced, don't worry, on s'en occuppera anyway après" ok = True elif (iddestinataire == 0) and typ in ["retrait", "transfert"] and (solde_destinataire_apres >= 0): ok = True ### WARNING : A ENLEVER APRES POT VIEUX ### #ok = True ########################################### if ok: if (solde_emetteur_apres >= solde_tres_negatif or (idemetteur <= 0) # idemetteur <= 0 assure que le BdE (et les notes spéciales) pourront toujours émettre de l'argent or doitanyway): # Là, pas de problème insert_as_valide, method = True, "normal" elif solde_emetteur_apres >= solde_pas_plus_negatif: # Là il faut les droits forced, sinon la transaction est faite, mais non validée insert_as_valide, method = forced, "forced" else: # Là il faut les droits overforced, sinon la transaction est faite, mais non validée insert_as_valide, method = overforced, "overforced" if justtesting: renvoie = None else: renvoie = _insert_transaction(self, typ, idemetteur, iddestinataire, qte, montant, description, categorie, insert_as_valide, cantinvalidate, cur=cur) if insert_as_valide: reussite = "ok : %s" % (method,) # Si cette transaction le fait passer en négatif, on prévient l'adhérent par mail cur.execute("SELECT solde_mail_passage_negatif FROM configurations WHERE used;") seuil_mail = cur.fetchone()["solde_mail_passage_negatif"] if solde_emetteur_avant >= seuil_mail and solde_emetteur_apres < seuil_mail and idemetteur > 0: cur.execute("UPDATE comptes SET last_negatif = NOW() WHERE idbde = %s;", [idemetteur,]) mail.mail_passage_negatif(emetteur, seuil_mail, solde_emetteur_apres) else: reussite = "failed : %s needed" % (method,) # Si le curseur n'a pas été fourni par la fonction appelante, il est temps de le commiter if not cur_given: cur.execute("COMMIT;") return (reussite, renvoie) else: if not emetteur_ajour: raise ExceptionsNote.AdhesionExpired(idemetteur) elif not destinataire_ajour: raise ExceptionsNote.AdhesionExpired(iddestinataire) else: raise RuntimeError("Wait... what ?")
[docs]def _un_credit_ou_un_retrait(self, retrait, mode, compte, montant, commentaire, params_pay, doitanyway, cur=None): """Effectue un crédit ou un retrait (si ``retrait=True``) sans aucune vérification. Pour usage interne. Pour un crédit, retourne une liste contenant l'id de transaction, et l'id de chèque/virement/carte bancaire s'il y a lieu, None sinon. Voir les docs de :py:meth:`ServeurFonctions._un_credit` et :py:meth:`ServeurFonctions._un_retrait`. """ acl_forced, acl_overforced = self._has_acl("forced"), self._has_acl("overforced") if len(commentaire) != 0: commentaire = u" (%s)" % (commentaire,) if cur is None: con, cur = BaseFonctions.getcursor() cur_given = False else: cur_given = True to_do = { "cheque" : (-1, "cheques"), "virement" : (-3, "virements"), "soge" : (-3, "virements"), "especes" : (-2, None), "cb" : (-4, "carte_bancaires"), } id_exchanger, table = to_do[mode] if mode == "soge": params_pay["banque"] = u"Offre sogé" if retrait: doingwhat = u"retrait" idem, iddest = compte, id_exchanger else: doingwhat = u"crédit" idem, iddest = id_exchanger, compte base_comment = doingwhat + u" " + mode.replace(u"cheque", u"chèque").replace(u"soge", u"sogé").replace(u"especes", u"espèces").replace(u"cb", u"carte bancaire") commentaire = base_comment + commentaire params_pay["retrait"] = retrait # on tente d'effectuer la transaction result, idtransaction = _une_transaction(self, doingwhat, idem, iddest, 1, montant, commentaire, doingwhat.title(), acl_forced, acl_overforced, doitanyway=doitanyway, cur=cur) success = (result.split()[0] == "ok") log_ids = [idem, iddest, idtransaction] # Si il y a une table liée if table: # On enregistre le chèque/virement/CB avec l'idtransaction et l'idbde params_pay["idtransaction"] = idtransaction params_pay["idbde"] = compte cur.execute(""" INSERT INTO %s (nom, prenom, banque, idtransaction, idbde, retrait) VALUES (%%(nom)s, %%(prenom)s, %%(banque)s, %%(idtransaction)s, %%(idbde)s, %%(retrait)s) RETURNING id; """ % (table,), params_pay) log_ids.append(cur.fetchone()["id"]) else: log_ids.append(None) if not retrait: # Pour un crédit, on ne logue pas, parce qu'il a pu être provoqué par une inscription qui logue elle-même return result, log_ids # On log, debug, send retireoupas = "retire" * success + "ne retire pas" * (not(success)) if success: method = result.split()[-1] else: method = result.split()[-2] needoupas = "used" * success + "needed" * (not(success)) message = "%s %s %s en %s [%s %s] (commentaire : %s)" % (compte, retireoupas, montant, mode, method, needoupas, commentaire) self._log("_un_retrait", cur, message, log_ids) # On ment sur la fonction actuelle, mais dans ce cas on est forcément appelé par _un_retrait if not cur_given: # On ne COMMIT que si le curseur n'a pas été fourni par la fonction appelante cur.execute("COMMIT;") self._debug(1, u"_un_retrait : %s" % (message,)) if success: self._send("Retrait effectué.") else: # La transaction s'est mal passée, on le dit au client self._send(None, 300, u"Retrait échoué : solde après transaction trop faible (nécessite %s)." % method)
[docs]def _un_credit(self, mode, destinataire, montant, commentaire, params_pay={}, doitanyway=False, cur=None): """Aucune vérification, effectue vraiment le crédit. Ne peut pas être appelée par le client. Update en conséquence les soldes de -1, -2, -3 et du destinataire. Crée le cheque ou le virement si besoin. * ``mode`` = ``"cheque"``, ``"especes"``, ``"virement"`` ou ``"soge"`` * ``params_pay = {"nom": "Passoire", "prenom": "Toto", "banque": s"sogé"}`` """ return _un_credit_ou_un_retrait(self, False, mode, destinataire, montant, commentaire, params_pay, doitanyway, cur=cur)
[docs]def crediter(self, data): """``data = [<iddestinataire>, <montant>, <typ_paiement>, <params_pay>]`` Fait un crédit (pas de possibilité de faire plusieurs crédits à la fois). ``<params_pay> = {"nom": <nom>, "prenom": <prénom>, "banque": <banque>, ["comm"/"commentaire"/"motif" : "plouf plouf"]}`` (``<params_pay>`` peut rester vide pour un crédit espèces) _log relevant ids : idbde du destinataire, id de la transaction, id du moyen de paiement (ou NULL) """ if not((type(data) == list) and (len(data) == 4) and (type(data[0]) == type(data[1]) == int) and data[2] in ["especes", "cheque", "virement","cb"] # sogé c'est pas à la main, c'est directement fait par une inscription/réadhésion and (type(data[3]) == dict)): _badparam(self, u"crediter") return # On mémorise la version fournie pour pouvoir faire joujou avec avec mais loguer correctement datasaved = copy.deepcopy(data) # On vérifie qu'on n'est pas allé donner un idbde<0 if (data[0] <= 0): # on crédite pas non plus le Bde self._send(None, 301, u"idbde <= 0 (%s) interdit." % (data[0],)) self._debug(3, u"crediter : idbde <= 0 (%s). Abort." % (data[0],)) return # On vérifie qu'on crédite bien du positif if (data[1] < 0): self._send(None, 305, u"montant < 0 (%s) interdit." % (data[1],)) self._debug(3, u"crediter : montant < 0 (%s). Abort." % (data[1],)) return iddestinataire, montant, typ, params_pay = data # Pour les espèces, on n'a pas besoin de vérifier params_pay # Pour la carte bancaire, on n'a pas besoin de la banque if typ in ["cheque", "virement", "cb"]: if (params_pay.get("nom", "") == "") or (params_pay.get("prenom", "") == ""): self._debug(3, u"crediter : nom ou prenom non spécifié pour le paiement") self._send(None, 4, u"Nom et prénom doivent être spécifiés pour ce type de paiement.") return if typ in ["cheque", "virement"] and (params_pay.get("banque", "") == ""): self._debug(3, u"crediter : banque non spécifiée pour le paiement") self._send(None, 4, u"La banque doit être spécifiée pour ce type de paiement.") return if self._has_acl("credits"): try: # le doitanyway est à False parce que le crédit peut échouer si le compte n'est pas à jour d'adhésion con, cur = BaseFonctions.getcursor() # On reconstitue le commentaire commentaire = "".join(map(lambda key: dict.get(params_pay, key, ""), ["commentaire", "comm", "motif"])) result, log_ids = _un_credit(self, typ, iddestinataire, montant, commentaire, params_pay, cur=cur) self._log("crediter", cur, datasaved, log_ids) cur.execute("COMMIT;") self._debug(1, u"crediter %s à %s en %s (params = %s)" % (montant, iddestinataire, typ, params_pay)) self._send("Crédit effectué.") return except ExceptionsNote.Error404 as exc: # C'est que le compte n'existe pas self._debug(3, u"crediter : %s" % (exc.message.decode("utf-8"),)) self._send(None, 303, u"%s." % (exc.message.decode("utf-8"),)) except ExceptionsNote.AdhesionExpired as exc: # C'est que le compte n'est pas à jour d'adhésion # (et que les droits ne sont pas suffisants pour le noter ou que la transaction le passe en <0) self._debug(3, u"crediter : crédit failed, compte pas à jour d'adhésion (%s)" % (iddestinataire,)) self._send(None, 304, u"Compte %s pas à jour d'adhésion." % (iddestinataire,)) except ExceptionsNote.AccountBlocked as exc: self._debug(3, u"crediter : crédit failed, compte %s bloqué" % exc.idbde) self._send(None, 306, str(exc).decode("utf-8")) else: _pasledroit(self, "credits") return
[docs]def _une_conso(self, successes, idcompte, idbouton, qte, forced=False, overforced=False): """ Fait consommer un bouton à un compte. Ne peut pas être appelée par le client. Ne fait pas de vérification de droits (fait seulement des choses différentes en fonction de forced/overforced/rien). _log relevant ids : idemetteur, iddestinataire, idbouton, idtransaction """ # On va d'abord chercher les objets en question con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM boutons WHERE id = %s;", (idbouton,)) bouton = cur.fetchone() if bouton is None: raise ExceptionsNote.Error404("bouton inexistant : %s" % (idbouton,)) cur.execute("SELECT * FROM comptes WHERE idbde = %s;", (idcompte,)) compte = cur.fetchone() if compte is None: raise ExceptionsNote.Error404("compte inexistant : %s" % (idcompte,)) if (qte < 0): raise ExceptionsNote.TuTeFousDeMaGueule("qte < 0 dans _une_conso.") # On appelle la fonction qui effectue les transactions result, idtransaction = _une_transaction(self, u'bouton', idcompte, bouton["destinataire"], qte, bouton["montant"], bouton["label"], bouton["categorie"], forced, overforced, cur=cur) # maintenant il faut log/debug en fonction de comment ça s'est passé success = (result.split()[0] == "ok") if success: method = result.split()[-1] else: method = result.split()[-2] consommeoupas = "consomme" * success + "ne consomme pas" * (not(success)) explain = "[%s %s]" % (method, "used" * success + "needed" * (not(success))) self._log("_une_conso", cur, "%s %s %s*%s (idbouton = %s) %s" % ( idcompte, consommeoupas, qte, bouton["label"], idbouton, explain), [idcompte, bouton["destinataire"], idbouton, idtransaction]) cur.execute("COMMIT;") self._debug(1, u"_une_conso : %s %s %s*%s (idbouton = %s) %s" % ( idcompte, consommeoupas, qte, bouton["label"], idbouton, explain)) # Que la transaction se soit bien ou mal passée, on le rajoute dans la liste # successes qui sera envoyée au client par la fonction appelante une fois if "forced" in method and success: successes.append([140, [idbouton, idcompte], "La transaction a été effectué mais la note est en négatif sévère."]) # toutes les consos effectuées elif success: successes.append([0, [idbouton, idcompte], "Transaction effectuée"]) else: successes.append([300, [idbouton, idcompte], "Transaction échouée : solde après transaction trop faible, nécessite %s.\n" % (method,)]) return successes
[docs]def consos(self, data): """``data`` = liste de ``[<idbouton>, <idcompte>, <quantité>]`` Fait consommer tous les boutons à tous les comptes. Transmet une liste de ``[<retcode>, [<idbouton>, <idbde>], <errmsg>]`` correspondant au succès des différentes transactions demandées. """ if not((type(data) == list) and (len(data) > 0) and ([type(i) for i in data] == [list] * len(data)) and all([([type(j) for j in i] == [int, int, int]) for i in data]) ): _badparam(self, u"consos") return # On vérifie qu'on n'est pas allé donner des idbde ou des quantité négatifs for idbouton, idcompte, qte in data: if (idcompte < 0): self._send(None, 301, u"idbde < 0 (%s) interdit." % (idcompte,)) self._debug(3, u"consos : idbde < 0 (%s). Abort." % (idcompte,)) return if (qte < 0): self._send(None, 302, u"quantite<0 (%s) interdite." % (qte,)) self._debug(3, u"consos : quantite<0 (%s). Abort." % (qte,)) return if self._has_acl("consos"): # Pour ne pas avoir à faire plein d'accès à la base pour connaître les droits on le fait ici acl_forced, acl_overforced = self._has_acl("forced"), self._has_acl("overforced") # On prépare également une liste de code de retours successes = [] for idbouton, idcompte, qte in data: # Une conso unique est gérée par une autre fonction try: successes = _une_conso(self, successes, idcompte, idbouton, qte, acl_forced, acl_overforced) except ExceptionsNote.Error404 as exc: # C'est qu'un bouton ou un compte n'existe pas self._debug(3, u"consos : %s" % (exc,)) successes.append([303, [idbouton, idcompte], "%s.\n" % (exc,)]) # on n'arrête pas l'exéution, donc les autres transactions se feront except ExceptionsNote.AdhesionExpired as exc: # C'est que le compte n'est pas à jour d'adhésion # (et que les droits ne sont pas suffisants pour le noter ou que la transaction le passe en <0) self._debug(3, u"consos : conso failed, compte pas à jour d'adhésion (%s)" % (idcompte,)) successes.append([304, [idbouton, idcompte], "compte %s pas à jour d'adhésion (et droits insuffisants ou transaction <0)" % (idcompte,)]) # on n'arrête pas l'exécution, donc les autres transactions se feront except ExceptionsNote.AccountBlocked as exc: self._debug(3, u"consos : conso failed, compte %s bloqué" % exc.idbde) successes.append([306, [idbouton, idcompte], "compte %s bloqué" % (exc.idbde)]) # On renvoie la liste des succès/échecs self._send(successes) else: _pasledroit(self, "consos")
[docs]def _un_transfert(self, successes, idemetteur, iddestinataire, montant, qte, motif, forced=False, overforced=False): """ Effectue un seul transfert. Ne peut pas être appelée par le client. Ne fait pas de vérification de droits (fait seulement des choses différentes en fonction de forced/overforced/rien). Renvoie quand même un échec sur des montants<0. _log relevant ids : idemetteur, iddestinataire, idtransaction """ # On vérifie montant >= 0 if (montant < 0): successes.append([304, [idemetteur, iddestinataire], "montant < 0 (%s) interdit.\n" % (montant,)]) self._debug(3, u"_un_transfert : montant négatif (%s) interdit. Abort." % (montant,)) return successes if len(motif) == 0: commentaire = u"transfert" else: commentaire = u"transfert (%s)" % (motif,) # On va d'abord chercher les comptes en question con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM comptes WHERE idbde = %s;", (idemetteur,)) emetteur = cur.fetchone() if emetteur is None: raise ExceptionsNote.Error404("compte inexistant : %s" % (idemetteur,)) cur.execute("SELECT * FROM comptes WHERE idbde = %s;", (iddestinataire,)) destinataire = cur.fetchone() if destinataire is None: raise ExceptionsNote.Error404("compte inexistant : %s" % (iddestinataire,)) # On appelle la fonction qui effectue les transactions result, idtransaction = _une_transaction(self, u'transfert', idemetteur, iddestinataire, qte, montant, commentaire, "Transfert", forced, overforced, cur=cur) # maintenant il faut log/debug en fonction de comment ça s'est passé success = (result.split()[0] == "ok") if success: method = result.split()[-1] else: method = result.split()[-2] transfertoupas = "transfert" * success + "ne transfert pas" * (not(success)) explain = "[%s %s]" % (method, "used" * success + "needed" * (not(success))) self._log("_un_transfert", cur, "%s %s %s à %s %s" % ( idemetteur, transfertoupas, montant, iddestinataire, explain), [idemetteur, iddestinataire, idtransaction]) cur.execute("COMMIT;") self._debug(1, u"_un_transfert : %s %s %s à %s %s" % ( idemetteur, transfertoupas, montant, iddestinataire, explain)) # Que la transaction se soit bien ou mal passée, on le rajoute dans la liste # successes qui sera envoyée au client par la fonction appelante une fois # tous les transferts effectués if "forced" in method and success: successes.append([300, [idemetteur, iddestinataire], "Attention : la transaction a été effectué mais la note est en négatif sévère."]) elif success: successes.append([0, [idemetteur, iddestinataire], "Transaction effectué"]) else: # La transaction s'est mal passée, on le dit au client successes.append([300, [idemetteur, iddestinataire], "Transaction échouée : solde après transaction trop faible, nécessite %s.\n" % (method,)]) return successes
[docs]def transferts(self, data): """``data = [<liste_d'émetteurs>, <liste_de_recepteurs>, <montant>, <motif>]`` Effectue le même transfert de chacun des émetteurs vers chacun des destinataires. Transmet une liste de ``[<retcode>, [<emetteur>, <destinataire>], <errmsg>]`` correspondant au succès des différentes transactions (autant que de #émetteurs * #destinataires). """ if not((type(data) == list) and (len(data) == 4) and (type(data[0]) == type(data[1]) == list) and (type(data[2]) == int) and (type(data[3]) == unicode) and (data[0] != []) and (data[1] != []) and all([type(i) == int for i in data[0]]) and all([type(i) == int for i in data[1]])): _badparam(self, u"transferts") return # On vérifie qu'on n'est pas entrain de faire joujou avec des idbde <0 if not(all([i >= 0 for i in data[0] + data[1]])): self._send(None, 301, u"idbde < 0 interdit.") self._debug(3, u"transferts : idbde < 0. Abort.") return # On vérifie que le montant n'est pas négatif if data[2]<0: self._send(None, 305, u"montant<0 (%s) interdit." % (data[2],)) self._debug(3, u"transferts : montant<0 (%s). Abort." % (data[2],)) return emetteurs, destinataires, montant, motif = data # On n'autorise pas un transfert concernant un club sans motif if len(motif) == 0: con, cur = BaseFonctions.getcursor() cur.execute("SELECT type FROM comptes WHERE idbde = ANY(%s);", (emetteurs + destinataires,)) types = [t["type"] for t in cur.fetchall()] if u"club" in types: self._send(None, 307, "Pour faire un transfert concernant un club, il faut préciser un motif.") self._debug(3, u"transferts : transfert sans motif concernant un club") return if self._has_acl("transferts"): # Pour ne pas avoir à faire plein d'accès à la base pour connaître les droits on le fait ici acl_forced, acl_overforced = self._has_acl("forced"), self._has_acl("overforced") # On prépare également une liste de code de retours successes = [] emetteurs = _factorize_idbde(emetteurs) destinataires = _factorize_idbde(destinataires) for emet, qte_e in emetteurs: for dest, qte_d in destinataires: # Un transfert unique est géré par une autre fonction try: # La quantité de transactions entre un ``emetteur`` et un ``destinataire`` qte = qte_e * qte_d successes = _un_transfert(self, successes, emet, dest, montant * qte, 1, motif, acl_forced, acl_overforced) except ExceptionsNote.Error404 as exc: # C'est qu'un compte n'existe pas self._debug(3, ("transferts : %s" % (exc,)).decode("utf-8")) successes.append([303, [emet, dest], ("%s.\n" % (exc,)).decode("utf-8")]) # on n'arrête pas l'exécution, donc les autres transactions se feront except ExceptionsNote.AdhesionExpired as exc: # C'est que le compte n'est pas à jour d'adhésion # (et que les droits nos sont pas suffisants pour le noter ou que la transaction le passe en <0) self._debug(3, u"transferts : transfert failed, le compte %s n'est pas a jour d'adhésion" % (exc)) successes.append([304, [emet, dest], u"Echec, le compte %s n'est pas à jour d'adhésion" % (exc)]) # on n'arrête pas l'exéution, donc les autres transactions se feront except ExceptionsNote.AccountBlocked as exc: self._debug(3, u"transferts : transfert failed, compte %s bloqué" % (exc.idbde,)) successes.append([306, [idbouton, idcompte], "compte %s bloqué" % (exc.idbde)]) # on renvoie la liste des succès/échecs self._send(successes) else: _pasledroit(self, "transferts")
[docs]def _un_don(self, successes, iddestinataire, montant, motif): """ Effectue un seul don. Ne peut pas être appelée par le client. Ne fait pas de vérification de droits, empêche juste de finir en négatif. Renvoie quand même un échec sur des montants<0. _log relevant ids : idemetteur, iddestinataire, idtransaction """ # On commence par vérifier qu'on est pas un special user if (self.userid == "special"): raise ExceptionsNote.TuTeFousDeMaGueule("un special user ça fait pas joujou avec _un_don") # On vérifie montant >= 0 if (montant < 0): successes.append([304, iddestinataire, "montant < 0 (%s) interdit.\n" % (montant,)]) self._debug(3, u"_un_don : montant négatif (%s) interdit. Abort." % (montant,)) return successes # On va d'abord chercher le compte en question con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM comptes WHERE idbde = %s;", (iddestinataire,)) destinataire = cur.fetchone() if destinataire is None: raise ExceptionsNote.Error404("compte inexistant : %s" % (iddestinataire,)) idemetteur = self.userid # On appelle la fonction qui effectue les transactions result, idtransaction = _une_transaction(self, u'don', idemetteur, iddestinataire, 1, montant, "don (%s)" % (motif,), "Don", cur=cur) # maintenant il faut log/debug en fonction de comment ça s'est passé success = (result.split()[0] == "ok") if success: method = result.split()[-1] else: method = result.split()[-2] donneoupas = "donne" * success + "ne donne pas" * (not(success)) self._log("_un_don", cur, "%s %s %s à %s" % ( idemetteur, donneoupas, montant, iddestinataire), [idemetteur, iddestinataire, idtransaction]) cur.execute("COMMIT;") self._debug(1, u"_un_don : %s %s %s à %s" % ( idemetteur, donneoupas, montant, iddestinataire)) # Que la transaction se soit bien ou mal passée, on le rajoute dans la liste # successes qui sera envoyée au client par la fonction appelante une fois # tous les dons effectués if success: successes.append([0, iddestinataire, "Transaction effectuée"]) else: successes.append([300, iddestinataire, "Transaction échouée : solde après transaction trop faible.\n"]) return successes
[docs]def dons(self, data): """``data = [<liste_de_destinataires>, <montant>, <motif>]`` Effectue le même don vers chacun des destinataires. Renvoie une liste de ``[<retcode>, <iddestinataire>, <errmsg>]`` correspondant au succès des différentes transactions (une par destinataire). """ if not((type(data) == list) and (len(data) == 3) and (type(data[0]) == list) and (type(data[1]) == int) and (type(data[2]) == unicode) and (data[0] != []) and all([(type(i) == int) for i in data[0]])): _badparam(self, u"dons") return # On vérifie qu'on n'est pas entrain de faire joujou avec des idbde <0 if not(all([(i >= 0) for i in data[0]])): self._send(None, 301, u"idbde < 0 (%s) interdit." % (i,)) self._debug(3, u"dons : idbde < 0 (%s). Abort." % (i,)) return destinataires, montant, motif = data # On n'autorise pas les dons vers un club sans motif if len(motif) == 0: con, cur = BaseFonctions.getcursor() cur.execute("SELECT type FROM comptes WHERE idbde = ANY(%s);", (destinataires,)) types = [t["type"] for t in cur.fetchall()] if u"club" in types: self._send(None, 307, "Pour faire un don vers un club, il faut préciser un motif.") self._debug(3, u"dons : dons sans motif concernant un club") return if self._has_acl("dons") and self._myself(): # On prépare une liste de codes de retour successes = [] for dest in destinataires: # Un don unique est gérée par une autre fonction try: successes = _un_don(self, successes, dest, montant, motif) except ExceptionsNote.Error404 as exc: # C'est qu'un compte n'existe pas self._debug(3, u"dons : %s" % (exc,)) successes.append([303, dest, "%s.\n" % (exc,)]) # on n'arrête pas l'exéution, donc les autres transactions se feront except ExceptionsNote.AdhesionExpired as exc: # C'est que le compte n'est pas à jour d'adhésion # (et que les droits nos sont pas suffisants pour le noter ou que la transaction le passe en <0) self._debug(3, u"dons : don failed, compte pas à jour d'adhésion (%s)" % (exc.idbde,)) successes.append([304, dest, "compte %s pas à jour d'adhésion (et droits insuffisants ou transaction <0)" % (exc.idbde,)]) # on n'arrête pas l'exéution, donc les autres transactions se feront except ExceptionsNote.AccountBlocked as exc: self._debug(3, u"dons : don failed, compte %s bloqué" % exc.idbde) successes.append([306, [self.userid, dest], "compte %s bloqué" % (exc.idbde)]) # On renvoie la liste des succès/échecs self._send(successes) else: _pasledroit(self, "dons")
[docs]def _un_retrait(self, mode, emetteur, montant, commentaire, params_pay={}): """Aucune vérification, effectue vraiment le retrait. Ne peut pas être appelée par le client. Update en conséquence les soldes de -1, -2, -3 et de l'émetteur. Crée le cheque ou le virement si besoin. (en ``retrait = true``) * ``mode`` = ``"cheque"``, ``"especes"`` ou ``"virement"`` * ``paramas_pay = {"nom": "Passoire", "prenom": "Toto", "banque": "sogé"}`` """ _un_credit_ou_un_retrait(self, True, mode, emetteur, montant, commentaire, params_pay, doitanyway=False)
[docs]def retirer(self, data): """``data = [<idemetteur>, <montant>, <typ_paiement>, <params_pay>]`` Fait un retrait (pas de possibilité de faire plusieurs retraits à la fois). ``<params_pay> = {"nom": <nom>, "prenom": <prénom>, "banque": <banque>, ["comm"/"commentaire"/"motif" : "plouf plouf"]}`` (``<params_pay>`` peut rester vide pour un retrait espèces). """ if not((type(data) == list) and (len(data) == 4) and (type(data[0]) == type(data[1]) == int) and (data[2] in ["especes", "cheque", "virement"]) and (type(data[3]) == dict)): _badparam(self, u"retirer") return # On vérifie qu'on n'est pas allé donner un idbde<0 if (data[0] <= 0): # on ne peut pas retirer non plus au Bde self._send(None, 301, u"idbde <= 0 (%s) interdit." % (data[0],)) self._debug(3, u"retirer : idbde <= 0 (%s). Abort." % (data[0],)) return # On vérifie qu'on retire bien du positif if (data[1] < 0): self._send(None, 305, u"montant < 0 (%s) interdit." % (data[1],)) self._debug(3, u"retirer : montant < 0 (%s). Abort." % (data[1],)) return idemetteur, montant, typ, params_pay = data # Pour les espèces, on n'a pas besoin de vérifier params_pay if (typ != "especes"): if not(set(["nom", "prenom", "banque"]).issubset(params_pay.keys())): _badparam(self, u"retirer (params paiment)") return if (params_pay["nom"] == "") or (params_pay["prenom"] == ""): self._debug(3, u"retirer : nom ou prenom non spécifié pour le paiement") self._send(None, 4, u"Nom et prénom doivent être spécifiés pour le paiement.") return if self._has_acl("retraits"): try: # On reconstitue le commentaire commentaire = "".join(map(lambda key: dict.get(params_pay, key, ""), ["commentaire", "comm", "motif"])) _un_retrait(self, typ, idemetteur, montant, commentaire, params_pay) return except ExceptionsNote.Error404 as exc: # C'est que le compte n'existe pas self._debug(3, u"retirer : %s\n" % (exc,)) self._send(None, 303, str(exc).decode("utf-8")) except ExceptionsNote.AdhesionExpired as exc: # C'est que le compte n'est pas à jour d'adhésion # (et que les droits ne sont pas suffisants pour le noter ou que la transaction le passe en <0) self._debug(3, u"retirer : retrait failed, compte pas à jour d'adhésion (%s)" % (idemetteur,)) self._send(None, 304, u"Compte %s pas à jour d'adhésion." % (idemetteur,)) except ExceptionsNote.AccountBlocked as exc: self._debug(3, u"retirer : retrait failed, compte %s bloqué" % exc.idbde) self._send(None, 306, str(exc).decode("utf-8")) else: _pasledroit(self, "retraits") return
[docs]def alias(self, data): """ ``data = [<idbde>, <alias>]`` Ajoute un alias à un compte. _log relevant ids : idbde, idalias """ if not ((type(data) == list) and (len(data) == 2) and (type(data[0]) == int) and (type(data[1]) == unicode)): _badparam(self, u"alias") return idbde, alias = data if self._has_acl("aliases") or self._myself(idbde): # On vérifie qu'il existe. con, cur = BaseFonctions.getcursor() cur.execute("SELECT idbde FROM comptes WHERE idbde = %s;", (idbde,)) l = cur.fetchall() if (len(l) == 0): self._debug(3, u"alias : failed. Idbde Unknown (%s)." % (idbde,)) self._send(None, 404, u"Ajout d'alias échoué, idbde %s inconnu." % (idbde,)) return if BaseFonctions.pseudo_libre(alias, idbde): # on peut s'ajouter en alias un pseudo qu'on a récemment utilisé # On expire les historiques qui sont ce pseudo Consistency.dereference_historique_pseudo(alias, cur) cur.execute("INSERT INTO aliases (alias, idbde) VALUES (%s, %s) RETURNING id;", (alias, idbde)) idalias = cur.fetchone()["id"] self._log("alias", cur, data, [idbde, idalias]) cur.execute("COMMIT;") self._debug(1, u"Ajout de l'alias %s à %s." % (alias, idbde)) self._send(u"Ajout d'alias effectué.") else: self._debug(3, u"alias : failed. Pseudo %s déjà utilisé" % (alias,)) self._send(None, 12, u"Ajout d'alias échoué, pseudo %s déjà pris." % (alias,)) return else: _pasledroit(self, "aliases")
[docs]def unalias(self, data): """ ``data = [<id>, <booléen all>]`` * Si ``all = False`` : supprime l'alias d'id ``<id>`` * Si ``all = True`` : supprime tous les alias du compte d'idbde ``<id>`` _log relevant ids : l'id il faudrait regarder dans la colonne params pour savoir si data contenait un idbde ou un idalias """ if not ((type(data) == list) and ([type(i) for i in data] == [int, bool])): _badparam(self, u"unalias") return ident, delete_all = data he_can = self._has_acl("aliases") or (delete_all and self._myself(ident)) if not he_can and not delete_all and not (self.userid == "special"): # Il a peut-être encore une chance si c'est son propre alias con, cur = BaseFonctions.getcursor() cur.execute("SELECT idbde FROM aliases WHERE id = %s;", (ident,)) alias = cur.fetchone() if alias: he_can = self._myself(alias["idbde"]) else: self._send(None, 404, u"Cet alias n'existe pas.") self._debug(3, u"unalias fail : id inexistant (%s)" % (ident,)) return if he_can: con, cur = BaseFonctions.getcursor() if delete_all: cur.execute("DELETE FROM aliases WHERE idbde = %s;", (ident,)) debugmsg = u"unalias : aliases de %s supprimés" % (ident,) else: cur.execute("DELETE FROM aliases WHERE id = %s;", (ident,)) debugmsg = u"unalias : alias n°%s supprimé" % (ident,) # NB :en ayant exécuté une commande pareille, on n'est pas sûr d'avoir # vraiment fait quelque chose. DELETE s'exécute sans erreur # même si WHERE n'est jamais vrai. self._log("unalias", cur, data, [ident]) cur.execute("COMMIT;") self._debug(1, debugmsg) self._send(u"Suppression d'alias effectuée.") else: _pasledroit(self, "aliases")
[docs]def get_activites(self, data): """``data = [<terme de recherche>, <flags>]`` ou ``[<terme>]`` ou ``<terme>`` Transmet la liste des activités. Les flags : * ``m`` : renvoie seulment les activités soumises par l'utilisateur courant (donne du coup accès aux non validées). * ``A`` : administration (affiche aussi les activités non validées) (``A`` écrase ``m``) * ``o`` : renvoie aussi les activités passées de l'année en cours. (par défaut, donne seulement les activités ``debut>now()``) (``o`` écrase ``A``) Les flags A et o nécessitent les droits activites_admin Chaque retour a un champ ``"invitable"`` dans lequel on a ``[<on peut inviter>, <liste de keywords des tests qui ont fail>]``. La liste n'étant pas forcément vide dans le cas où on peut inviter (un admin peut bypasser certains tests). """ if data is None: term, flags = "", "" elif (type(data) == unicode): term, flags = data, "" elif (type(data) == list) and (len(data) == 1) and (type(data[0]) == unicode): term, flags = data[0], "" elif (type(data) == list) and (len(data) == 2) and (type(data[0]) == type(data[1]) == unicode): term, flags = data else: _badparam(self, u"get_activites") return ask_old, mine, ask_admin = "o" in flags, "m" in flags, "A" in flags isadmin = ask_admin and self._has_acl("activites_admin") isold = ask_old and self._has_acl("activites_admin") if isold: time_clause = "debut>date_trunc('year', now())" else: time_clause = "fin > now()" if isadmin: # l'administration des activités demande plus de droits condition_clause = "true" elif mine and (self.userid != "special") and self._myself(): # un special user ne peut pas enregistrer d'activité, # et on ne veut pas pouvoir accéder à ses activités si on n'a pas le droit "myself" condition_clause = "responsable = %(userid)s" else: condition_clause = "validepar IS NOT NULL" # par défaut, validepar est à NULL if (term == ""): search_clause = "extract(year FROM debut)>=extract(year FROM now())" else: search_clause = "(titre ILIKE '%%%(term)s%%' OR description ILIKE '%%%(term)s%%')" if self._has_acl("activites"): req = """SELECT activites.* FROM activites WHERE """ + search_clause + " AND " + time_clause + " AND " + condition_clause + " ORDER BY debut;" con, cur = BaseFonctions.getcursor() cur.execute(req, {"userid": self.userid, "term": term}) l = [dict(i) for i in cur.fetchall()] # on rajoute le champ "invitable" if (self.userid == "special"): # on doit truander respo = {"idbde": "0", "solde": 100} else: respo = ReadDatabase.get_compte(self.userid) for i in range(len(l)): l[i]["invitable"] = _invitable(self, l[i]["id"], respo, isadmin)[0:2] # les 2 autres champs servent ailleurs self._debug(4, u"Demande des activités : %s, %s"%(term, flags)) self._send(l) else: _pasledroit(self, "activites")
[docs]def get_activite(self, data): """``data = <id>`` Transmet les informations sur l'activité demandée. """ if not(type(data) == int): _badparam(self, u"get_activite") return idact = data isadmin = self._has_acl("activites_admin") # On va d'abord chercher l'activité dans la base # (ça ne veut pas forcément dire que l'utilisateur a le droit de la voir) con, cur = BaseFonctions.getcursor() cur.execute("""SELECT activites.*, (SELECT pseudo FROM comptes WHERE idbde = activites.validepar) AS valideparpseudo, (SELECT pseudo FROM comptes WHERE idbde = activites.responsable) AS responsablepseudo FROM activites WHERE id = %s;"""%(idact,)) activite = cur.fetchone() if activite is None: self._send(None, 404, u"Cette activité n'existe pas.") return # On peut voir une activité si : # on est admin # OU (elle n'a pas encore commencé ET est validée) # OU (c'est la mienne ET j'ai accès à mon compte) can_see = (isadmin or ((activite["debut"].timetuple() > time.localtime()) and (activite["validepar"] is not None)) or self._myself(activite["responsable"]) ) if can_see: self._send(dict(activite)) self._debug(4, u"get_activite : envoi de l'activité %s" % (idact,)) else: self._send(None, 403, u"Tu n'as pas le droit de voir cette activité.") self._debug(3, u"get_activite failed : pas le droit de voir l'activité %s" % (idact,))
[docs]def _handle_negative_duration(self, exc): """Si l'erreur est bien un problème de durée négative, transmet un message d'erreur, sinon relève l'exception.""" if "activite_a_une_duree_positive" in str(exc): self._debug(3, u"add_activite : activité à durée négative refusée") self._send(None, 701, u"La méthode DeLorean_TimeTravel() n'est pas encore implémentée dans la Note Kfet 2015, merci de réessayer plus tard ou bien de te résoudre à faire commencer ton activité avant qu'elle ne soit finie.") else: raise
[docs]def add_activite(self, data): """ Ajoute une activité. ``data`` = un dico avec les clés ``"debut"``, ``"fin"``, ``"titre"``, ``"lieu"``, ``"description"``, ``"signature"`` et ``"liste"`` _log relevant ids : idact """ dicotypes = {"debut": unicode, "fin": unicode, "titre": unicode, "lieu": unicode, "description": unicode, "signature": unicode, "liste": bool} if not((type(data) == dict) and (set(data.keys()) == set(dicotypes.keys())) and all([(type(data[i]) == dicotypes[i]) for i in data.keys()]) and (data["titre"] != "") and (data["lieu"] != "") and BaseFonctions.isPgsqlDate(data["debut"]) and BaseFonctions.isPgsqlDate(data["fin"])): _badparam(self, u"add_activite") return if self._has_acl("activites") and self._myself(): data["responsable"] = self.userid req = """ INSERT INTO activites (debut, fin, titre, lieu, description, signature, liste, responsable) VALUES (%(debut)s, %(fin)s, %(titre)s, %(lieu)s, %(description)s, %(signature)s, %(liste)s, %(responsable)s) RETURNING id; """ con, cur = BaseFonctions.getcursor() try: cur.execute(req, data) idact = cur.fetchone()["id"] self._log("add_activite", cur, data, [idact]) cur.execute("COMMIT;") self._debug(1, u"activitée ajoutée : %s" % (data,)) self._send("Activité ajoutée.") keys = ["titre", "pseudo", "signature", "description", "debut", "fin", "lieu", "liste"] req = """ SELECT * FROM activites INNER JOIN comptes ON responsable = idbde WHERE validepar ISNULL ORDER BY id DESC ; """ cur.execute(req) details_activites = cur.fetchall() nouvelle_activite = BaseFonctions.sql_pretty_print([details_activites[0]], keys) if len(details_activites)>1: non_valides = u"Pour rappel, les activités suivantes sont toujours en attente de validation :\n" non_valides += BaseFonctions.sql_pretty_print(details_activites[1:], keys) else : non_valides = "" mail.mail_new_activity(nouvelle_activite, non_valides) except psycopg2.IntegrityError as exc: _handle_negative_duration(self, exc) else: _pasledroit(self, "activites")
[docs]def update_activite(self, data): """ Modifie une activité. ``data`` = ``[<un dico contenant au moins le champs "id">, <flag "A" facultatif>]`` Champs possiblement modifiables : ``"debut"``, ``"fin"``, ``"titre"``, ``"lieu"``, ``"description"``, ``"signature"``, ``"liste"``, ``"listeimprimee"`` On ne peut modifier une activité que si : * on en est le responsable et qu'elle n'a pas été validée * ou bien on a les droits activites_admin (et on les a demandés) Les champs ``"listimprimee"`` et ``"responsable"`` nécessitent de toutes façons les droits activites_admin _log relevant ids : idact """ if not((type(data) == list) and len(data) in [1, 2] and (type(data[0]) == dict)): _badparam(self, u"update_activite") return if (len(data) == 1): data, ask_admin = data[0], False else: data, ask_admin = data[0], (data[1] == "A") isadmin = ask_admin and self._has_acl("activites_admin") dicotypes = {"id": int, "debut": unicode, "fin": unicode, "titre": unicode, "lieu": unicode, "description": unicode, "signature": unicode, "liste": bool, "listeimprimee": bool} if not ((type(data) == dict) and set(data.keys()).issubset(dicotypes.keys()) and "id" in data.keys() and all([(type(data[i]) == dicotypes[i]) for i in data.keys()])): _badparam(self, u"update_activite") return if self._has_acl("activites"): if ("listeimprimee" in data.keys() or "responsable" in data.keys()) and not isadmin: # champ nécessitant les droits d'administration des activités _pasledroit(self, "activites_admin") return # on va récupérer l'activité con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM activites WHERE id = %s;", (data["id"],)) l = cur.fetchall() if (l == []): self._debug(3, u"update_activite failed : id inexistant (%s)" % (data["id"],)) self._send(None, 404, u"Id d'activité inexistant.") else: activite = dict(l[0]) # Dernière vérification : seul un admin peut modifier une activité validée # et si elle ne l'est pas, seul son reponsable peut la modifier myactivite = self._myself(activite["responsable"]) if not(isadmin or (myactivite and (activite["validepar"] is None))): if not myactivite: self._send(None, 731, u"Tu ne peux pas modifier cette activité : tu n'en es pas le responsable (ou tu n'es pas connecté avec tes droits).") self._debug(3, u"update_activites failed : %s n'est pas responsable de l'activité %s (ou ne s'est pas connecté avec ses droits)" % (self.userid, data["id"])) else: self._send(None, 732, u"Tu ne peux pas modifier cette activité : elle a été validée.") self._debug(3, u"update_activites failed : l'activité %s est validée" % (data["id"],)) return # on la modifie avec les infos fournies activite.update(data) # on la réenregistre try: cur.execute("""UPDATE activites SET debut = %(debut)s, fin = %(fin)s, titre = %(titre)s, lieu = %(lieu)s, description = %(description)s, signature = %(signature)s, liste = %(liste)s, listeimprimee = %(listeimprimee)s WHERE id = %(id)s;""", activite) self._log("update_activite", cur, data, [activite["id"]]) cur.execute("COMMIT;") self._debug(1, u"update_activite done : %s" % (data,)) self._send("Activité modifiée.") except psycopg2.IntegrityError as exc: _handle_negative_duration(self, exc) else: _pasledroit(self, "activites")
[docs]def del_activite(self, data): """ ``data = [<id>, <flag "A" facultatif>]`` Supprime une activité. _log relevant ids : idact """ if not((type(data) == list) and len(data) in [1, 2] and (type(data[0]) == int) and (data[0] > 0)): _badparam(self, u"del_activite") return if (len(data) == 1): idact, ask_admin = data[0], False else: idact, ask_admin = data[0], (data[1] == "A") isadmin = ask_admin and self._has_acl("activites_admin") if self._has_acl("activites"): # On peut supprimer sa propre activité si elle n'est pas encore validée # donc on va d'abord chercher l'activité con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM activites WHERE id = %s;", (idact,)) l = cur.fetchall() if (l == []): # on vérifie qu'elle existe self._debug(3, u"del_activite failed : id inexistant (%s)" % (idact,)) self._send(None, 404, u"Id d'activité inexistant.") else: activite = l[0] if ((self._myself(activite["responsable"]) and (activite["validepar"] is None)) or isadmin): # on peut la virer si on en est le responsable et qu'elle n'est pas validée # ou si on a les droits d'administration (et qu'on les a demandés) # Mais il faut d'abord vérifier que personne n'y a été invité cur.execute("SELECT count(id) FROM invites WHERE activite = %s;", (idact,)) if (cur.fetchone()[0] != 0): self._debug(3, u"del_activite failed : il y a des invités (idact : %s)" % (idact,)) self._send(None, 720, u"Impossible de supprimer l'activité, des gens y sont invités (supprime-les d'abord).") return cur.execute("DELETE FROM activites WHERE id = %s;", (idact,)) self._log("del_activite", cur, idact, [idact]) cur.execute("COMMIT;") self._debug(1, u"activité %s supprimée" % (idact,)) self._send("Activité supprimée.") else: if not self._myself(activite["responsable"]): self._debug(3, u"del_activite failed : %s n'est pas le responsable de l'activité %s (ou n'est pas connecté avec ses droits)" % (self.userid, idact)) self._send(None, 721, u"Tu ne peux pas supprimer cette activité : tu n'en es pas le responsable (ou tu n'es pas connecté avec tes droits).") else: self._debug(3, u"del_activite failed : activité %s validée" % (idact)) self._send(None, 722, u"Tu ne peux pas supprimer cette activité : elle a déjà été validée.") else: _pasledroit(self, "activites")
[docs]def _get_activity_overlap(idact, cur): """Renvoie la liste des activités déjà validées qui entrent en collision avec l'activité n°``idact``""" cur.execute("SELECT * FROM activites WHERE id = %s;", (idact,)) activite = cur.fetchone() if activite is not None: cur.execute("""SELECT * FROM activites WHERE validepar IS NOT NULL AND NOT( fin <= %(debut)s OR %(fin)s <= debut OR id = %(id)s);""", activite) return cur.fetchall() else: return []
[docs]def valider_activite(self, data): """ ``data = <id>`` Valider une activité. NB : N'échoue pas si l'activité n'existe pas ou si elle était déjà validée. _log relevant ids : idact """ if not((type(data) == int) and (data >= 0)): _badparam(self, u"valider_activite") return if self._has_acl("activites_admin") and self._myself(): con, cur = BaseFonctions.getcursor() cur.execute("UPDATE activites SET validepar = %s WHERE id = %s;", (self.userid, data)) # On regarde les activités en conflit retcode, errmsg = 0, "" conflicts = _get_activity_overlap(data, cur) if conflicts: retcode, errmsg = 130, u"L'activité a bien été validée mais les activités suivantes la chevauchent :\n" for act in conflicts: errmsg += u"%s (du %s au %s), lieu : %s" % (act["titre"], act["debut"].strftime("%d/%m/%Y à %T").decode('utf-8'), act["fin"].strftime("%d/%m/%Y à %T").decode('utf-8'), act["lieu"]) self._log("valider_activite", cur, data, [data]) cur.execute("COMMIT;") self._debug(1, u"valider_activite : activité %s validée%s" % (data, " (avec overlap)" * (conflicts != []))) self._send("Activité validée.", retcode, errmsg) # On modifie le wiki. Attention, en cas d'échec, le client n'en saura rien. Wiki.refresh_calendar(u"validation") else: _pasledroit(self, "activites_admin")
[docs]def devalider_activite(self, data): """ ``data = <id>`` Dévalider une activité. NB : N'échoue pas si l'activité n'existe pas ou si elle n'était déjà pas validée. _log relevant ids : idact """ if not((type(data) == int) and (data >= 0)): _badparam(self, u"devalider_activite") return if self._has_acl("activites_admin"): con, cur = BaseFonctions.getcursor() cur.execute("UPDATE activites SET validepar = NULL WHERE id = %s;", (data,)) self._log("devalider_activite", cur, data, [data]) cur.execute("COMMIT;") self._debug(1, u"devalider_activite : activité %s dévalidée" % (data,)) self._send("Activité dévalidée.") # On modifie le wiki. Attention, en cas d'échec, le client n'en saura rien. Wiki.refresh_calendar(u"dévalidation") else: _pasledroit(self, "activites_admin")
[docs]def _invitable(self, idact, respo, isadmin, nom=None, prenom=None): """Ne peut pas être appelée par le client. Peut être utilisé sans préciser ``nom`` ni ``respo``, et alors permet de savoir si l'utilisateur courant a le droit d'inviter *quelqu'un* à cette activité. (NB : le test des [3] invités par inviteur ne sera alors pas fait. On veut qu'un utilisateur puisse voir/supprimer ses invités même si il en a déjà [3]) Renvoie ``[<boolén ok/pas ok pour inviter>, <une liste de keywords>, <a>, <b>]`` : ((<a>, <b>) vaut (max_pers, max_an) si on eu le temps d'atteindre les tests les concernant, (0, 0) sinon) Explication des keywords : * ``"<fail>"`` = <signification> (<seul/cumulable>) [True] [True] est présent si ce champ n'empêche pas la réponse d'être True. Typiquement, si on a déjà 3 invités ou si on est en négatif, on ne peux plus inviter mais on peut quand même voir ses invités. * ``"404"`` : cette activité n'existe pas (seul) * ``"nolist"`` : la liste d'invités de cette activité n'existe pas ou n'est pas accessible (activité non validée, ou liste imprimée) (seul) * ``"already invited"`` : cette personne a déjà été invitée à cette activité (seul) * ``"closed"`` : la liste n'est plus ou pas encore ouverte (cumulable) * ``"negatif"`` : l'inviteur est en négatif, il n'a pas le droit d'inviter (cumulable) [True] * ``"max perso"`` : l'inviteur a déjà invité [3] invités à cette activité (cumulable) [True] * ``"max annee"`` : cet invité a déjà invité [5] fois dans l'année (cumulable) """ if (nom==prenom==None): # c'est qu'on demande si on peut avoir accès à la liste d'invités de cette activité about_activite=True else: # c'est qu'on cherche si on a le droit d'inviter une personne en particulier about_activite=False idrespo = respo["idbde"] # on va d'abord chercher l'activité con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM activites WHERE id = %s;", (idact,)) l = cur.fetchall() # on vérifie qu'elle existe if (l == []): return [False, ["404"], 0, 0] activite = l[0] # on vérifie qu'il y a bien une liste pour cette activité, qu'elle est validée, # et que la liste n'a pas encore été imprimée if not (activite["liste"] and (activite["validepar"] is not None) and not(activite["listeimprimee"])): return [False, ["nolist"], 0, 0] # on vérifie que l'invité n'existe pas déjà cur.execute("SELECT * FROM invites WHERE nom = %s AND prenom = %s AND activite = %s;", (nom, prenom, idact)) l = cur.fetchall() if not(l == []): return [False, ["already invited"], 0, 0] # ensuite, soit on est admin, soit il y a des conditions en plus à vérifier # de toutes façons on les vérifie quand même (pour pouvoir envoyer des warnings) fails = [] # Les conditions sont : # 1) la liste n'est ouverte que pendant une certaine période avant le début de l'activité # [on récupère également la config pour les maxs d'invitation # et solde_negatif # anisi que les dates de year_boot] cur.execute("""SELECT liste_invites_opening_time, liste_invites_closing_time, max_invitation_par_personne, max_invitation_par_an, solde_negatif, year_boot_day, year_boot_month FROM configurations WHERE used = true;""") l = cur.fetchall() open_time, close_time, max_personne, max_an, solde_negatif, ybd, ybm = l[0] now = time.time() debut = time.mktime(activite["debut"].timetuple()) ouverture = debut - open_time * 3600 # les valeurs sont en heures fermeture = debut - close_time * 3600 if not (ouverture < now < fermeture): fails.append("closed") # 2) le responsable doit être en positif ## Ce test n'est pas fait si nom, prenom ne sont pas fournis if (nom is not None) and (prenom is not None): respo_solde = respo["solde"] if not(respo_solde >= solde_negatif): fails.append("negatif") # 3) il n'a pas déjà invité trop de gens à cette activité cur.execute("""SELECT count(id) AS nb_pers FROM invites WHERE responsable = %s AND activite = %s;""", (idrespo, idact)) nb_pers = cur.fetchall()[0]["nb_pers"] if (nb_pers >= max_personne): # 3 fails.append("max perso") # 4) l'invité en question n'a pas déjà été invité trop de fois dans l'année # "dans l'année" = depuis le précédent year_boot jusqu'au suivant ## Ce test n'est pas fait si nom, prenom ne sont pas fournis if (nom is not None) and (prenom is not None): now = time.localtime() now_y, now_m, now_d = now[0:3] if (now_m, now_d) < (ybm, ybd): # si on est avant le year_boot, en fait il faut enlevé 1 à l'année now_y -= 1 debut_annee = "%s-%s-%s" % (now_y, ybm, ybd) fin_annee = "%s-%s-%s" % (now_y + 1, ybm, ybd) cur.execute("""SELECT count(inv.id) AS nb_an FROM invites as inv, activites as act WHERE inv.activite = act.id AND %s < act.debut AND act.debut < %s AND inv.nom = %s AND inv.prenom = %s;""", (debut_annee, fin_annee, nom, prenom)) nb_an = cur.fetchall()[0]["nb_an"] if (nb_an >= max_an): # 4 fails.append("max annee") if about_activite: # si on demande à voir la liste d'une activité, on peut la voir # si on est admin ou si les fails restent cantonné à négatif/on en a déjà invité [3] caninvite = isadmin or set(fails).issubset(["negatif", "max perso"]) else: caninvite = isadmin or (len(fails) == 0) return [caninvite, fails, max_personne, max_an]
[docs]def add_invite(self, data): """ ``data = [<data2>, <flag "A" facultatif>]`` Ajouter un invité à une activité. ``data2 = [<nom>, <prenom>, <id activité>]`` ou ``data2 = [<nom>, <prenom>, <id activité>, <id responsable>]`` mais il faut les droits invites_admin Pour pouvoir inviter, il faut : * que l'activité ait une liste et qu'elle soit validée * que la liste ne soit pas imprimée * que l'invité n'existe pas déjà pour cette activité * si on n'est pas admin, il faut vérifier : * ``now() < debut - configurations.liste_invites_closing_time`` * ``now() > debut - configurations.liste_invites_opening_time`` * ``current_user.solde >= configurations.solde_negatif`` * ``current_user`` a invité strictement moins de configurations.max_invitation_par_personne personnes à cette activité * ``<nom>``, ``<prenom>`` a été invité strictement moins de ``configurations.max_invitation_par_an`` fois cette année _log relevant ids : id de l'invité ajouté, id du responsable de l'invité """ if not((type(data) == list) and len(data) in [1, 2] and (type(data[0]) == list)): _badparam(self, u"add_invite") return if (len(data) == 1): data, ask_admin = data[0], False else: data, ask_admin = data[0], (data[1] == "A") isadmin = ask_admin and self._has_acl("invites_admin") if not((type(data) == list) and [type(i) for i in data] in ([unicode, unicode, int], [unicode, unicode, int, int]) and (len(data[0]) > 0) and (len(data[1]) > 0)): _badparam(self, u"add_invite") return if (len(data) == 4) and isadmin: nom, prenom, idact, idrespo = data elif (len(data) == 3): nom, prenom, idact = data if self.userid == "special": self._send(None, 711, u"Pour inviter, un special user doit préciser le champ responsable.") self._debug(3, u"add_invite failed : special user qui n'a pas précisé responsable") return else: idrespo = self.userid respo = ReadDatabase.get_compte(idrespo) nom, prenom = nom.title(), prenom.title() if self._has_acl("invites") and self._myself(): # on demande à une fonction spécialement faite pour ça # si l'utilisateur courant peut inviter à cette activité can_invite, keywords, max_pers, max_an = _invitable(self, idact, respo, isadmin, nom, prenom) if "404" in keywords: self._send(None, 404, u"Activité inexistante.") self._debug(3, u"add_invite failed : id activité inconnu (%s)" % (idact,)) return if "nolist" in keywords: self._send(None, 710, u"Impossible d'inviter à cette activité.") self._debug(3, u"add_invite failed : liste inexistante ou inaccessible pour l'activité %s" % (idact,)) return if "already invited" in keywords: self._send(None, 715, u"Cette personne a déjà été invitée à cette activité.") self._debug(3, u"add_invite failed : %s %s déjà invité à l'activité %s" % (prenom, nom, idact)) return ## Les tests qui suivent peuvent être outrepassés par un admin # on génère aussi un message d'erreur, pour prévenir l'admin qu'il est sorti des sentiers autorisés test_failed, errmsg = False, "" if "closed" in keywords: # 1 if not isadmin: self._send(None, 712, u"Liste non ouverte à l'heure actuelle.") self._debug(3, u"add_invite failed : liste non ouverte à cette heure") return errmsg += u"La liste n'est pas encore ouverte ou déjà fermée\n" test_failed = True if "negatif" in keywords: # 2 if not isadmin: self._send(None, 713, u"Tu es en négatif (%s €), tu ne peux pas inviter." % (respo["solde"]/100.0,)) self._debug(3, u"add_invite failed : %s est en négatif" % (idrespo,)) return errmsg += u"%s est en négatif, il/elle ne devrait pas inviter\n" % (respo["pseudo"],) test_failed = True if "max perso" in keywords: # 3 if not isadmin: self._send(None, 714, u"Tu as déjà invité %s personnes à cette activité." % (max_pers,)) self._debug(3, u"add_invite failed : %s a déjà invité %s personnes à l'activité" % (idrespo, max_pers)) return errmsg += u"%s a déjà invité %s personnes à cette activité, il ne devrait pas pouvoir en inviter plus\n" % (respo["pseudo"], max_pers) test_failed = True if "max annee" in keywords: # 4 if not isadmin: self._send(None, 715, u"%s %s a déjà été invité %s fois cette année." % (prenom, nom, max_an)) self._debug(3, u"add_invite failed : %s %s a déjà été invité %s fois cette année" % (prenom, nom, max_an)) return errmsg += u"%s %s a déjà été invité %s fois cette année, il ne devrait pas pouvoir être encore invité\n" % (prenom, nom, max_an) test_failed = True # Si on est parvenu ici c'est qu'on a franchi tous les tests # donc, c'est bon con, cur = BaseFonctions.getcursor() cur.execute(""" INSERT INTO invites (nom, prenom, responsable, activite) VALUES (%s, %s, %s, %s) RETURNING id; """, (nom, prenom, idrespo, idact)) idinv = cur.fetchone()["id"] self._log("add_invite", cur, [nom, prenom, idact, idrespo], [idinv, idrespo]) cur.execute("COMMIT;") if test_failed: self._debug(1, u"invité ajouté (avec bypass %s): %s %s à l'activité n°%s (responsable %s)" % (keywords, nom, prenom, idact, idrespo)) self._send("Invité ajouté (avec bypass).", 110, errmsg) else: self._debug(1, u"invité ajouté : %s %s à l'activité n°%s (responsable %s)" % (nom, prenom, idact, idrespo)) self._send("Invité ajouté.") else: _pasledroit(self, "invites")
[docs]def del_invite(self, data): """ ``data = [<id>, <flag "A" facultatif>]`` Supprimer un invité. Évidemment si on n'est pas admin, on ne peut supprimer qu'un invité qu'on a invité soi-même et avant que la liste ne soit imprimée ou fermée. _log relevant ids : id de l'invité """ if not((type(data) == list) and len(data) in [1, 2] and (type(data[0]) == int) and (data[0] > 0)): _badparam(self, u"del_invite") return if (len(data) == 1): idinv, ask_admin = data[0], False else: idinv, ask_admin = data[0], (data[1] == "A") isadmin = ask_admin and self._has_acl("invites_admin") # On va chercher l'invité con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM invites WHERE id = %s;" % (idinv)) l = cur.fetchall() if (l == []): self._send(None, 404, u"Cet invité n'existe pas.") self._debug(3, u"del_invite failed : id invité inexistant (%s)" % (idinv,)) return invite = l[0] if self._has_acl("invites"): # on va chercher l'activité correspondante cur.execute("SELECT * FROM activites WHERE id = %s;" % (invite["activite"],)) l = cur.fetchall() if (l == []): raise IntegrityError("Cet invité a été invité à une activité inexistante (%s)" % (idinv,)) activite = l[0] # soit on est admin, soit il faut vérifier d'autres conditions if not isadmin: # On ne peut pas supprimer un invité après l'heure de fermeture de la liste cur.execute("SELECT liste_invites_closing_time FROM configurations WHERE used = true;") close_time = cur.fetchall()[0][0] * 3600 closing = time.mktime(activite["debut"].timetuple()) - close_time if (time.time() > closing) or activite["listeimprimee"]: self._send(None, 712, u"La liste de cette activité est fermée, tu ne peux plus supprimer d'invité.") self._debug(3, u"del_invite failed : liste fermée (%s)" % (idinv,)) return # On ne peut supprimer un invité que si on l'a invité soi-même if not self._myself(invite["responsable"]): self._send(None, 714, u"Tu n'es pas le responsable de cet invité (ou tu n'es pas connecté avec tes droits), tu ne peux pas le supprimer.") self._debug(3, u"del_invite failed : %s n'est pas le responsable de l'invité %s (ou n'est pas connecté avec ses droits)" % (self.userid, idinv)) return # du coup, c'est bon cur.execute("DELETE FROM invites WHERE id = %s;" % (idinv,)) self._log("del_invite", cur, idinv, [idinv]) self._debug(1, u"del_invite : %s" % (idinv,)) self._send("Invité supprimé.") cur.execute("COMMIT;") else: _pasledroit(self, "invites")
[docs]def _get_invites(idact, responsable=None, compute_times=False): """Récupère la liste des invités d'une activité dans la base de données. Si ``responsable`` n'est pas ``None``, ne renvoie que les invités invités par ce responsable. Si ``compute_times`` est vrai, on rajoute un champs ``"times"`` qui compte le nombre de fois qu'un invité est venu dans l'année.""" if responsable is None: respoclause = "" else: respoclause = " AND responsable = %(responsable)s" con, cur = BaseFonctions.getcursor() if compute_times: # On inclut le nombre de fois que cet invité a été vu entre le year_boot précédant l'activité et le début de l'activité (inclu) cur.execute("""SELECT inv.id, inv.nom, inv.prenom, c.pseudo AS responsable, (SELECT count(*) FROM invites AS invcount WHERE invcount.nom = inv.nom AND invcount.prenom = inv.prenom AND invcount.activite in (SELECT id FROM activites WHERE activites.debut <= a.debut AND activites.debut >= previous_year_boot(CAST(a.debut AS date)) ) ) AS times FROM invites AS inv, comptes AS c, activites AS a WHERE activite = %(activite)s AND inv.responsable = c.idbde AND a.id = inv.activite""" + respoclause + " ORDER BY nom, prenom;", {"activite": idact, "responsable": responsable}) else: cur.execute("""SELECT inv.id, inv.nom, inv.prenom, c.pseudo AS responsable FROM invites AS inv, comptes AS c WHERE activite = %(activite)s AND inv.responsable = c.idbde """ + respoclause + " ORDER BY nom, prenom;", {"activite": idact, "responsable": responsable}) l = cur.fetchall() return l
[docs]def get_invites(self, data): """``data = [<id de l'activité>, <flag "A" facultatif>]`` Afficher les invités. Avec le flag ``A``, affichera tous les invités, pas seulement ceux invités par le ``current_user``. Renvoie aussi le pseudo du responsable dans le champ ``"pseudo"`` """ if (type(data) == int): idact, admin = data, False elif (type(data) == list) and ([type(i) for i in data] == [int, unicode]): idact, admin = data[0], "A" in data[1] else: _badparam(self, u"get_invites") return admin = admin and self._has_acl("invites_admin") if self._has_acl("invites") and self._myself(): if admin: responsable = None else: responsable = self.userid l = _get_invites(idact, responsable) self._send(l) self._debug(4, u"get_invites par %s pour l'activité %s" % (self.userid, idact)) else: _pasledroit(self, "invites")
[docs]def _make_html(template, dico_sur, dico_to_escape): """Fabrique une chaîne html en plaçant les valeur du dico dans le template mais en prenant soin de les échapper.""" # Pas de fonction python pour échaper du HTML, de toutes façons il n'y a que 3 caractères # puisque je n'ai pas besoin d'échapper les NON-ascii chars dico_to_escape = {k : unicode(v).replace(u"&", u"&amp;").replace(u"<", u"&lt;").replace(u">", u"&gt;") for (k, v) in dico_to_escape.iteritems()} dico = dico_sur dico.update(dico_to_escape) return template % dico
[docs]def _make_pdf_from_html(htmlraw): """Fabrique un pdf à partir d'une chaîne html. Renvoie (rawpdf, None) ou (None, message d'erreur). """ fhtml = tempfile.NamedTemporaryFile(suffix=".html", delete=False) fhtml.file.write(htmlraw.encode("utf-8")) fhtml.close() fpdf = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) proc = subprocess.Popen([config.binary_wkhtmltopdf, fhtml.name, fpdf.name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate() if proc.returncode != 0: return (None, err.decode("utf-8")) fpdf.close() rawpdf = open(fpdf.name).read() os.remove(fpdf.name) os.remove(fhtml.name) return (rawpdf, None)
[docs]def liste_invites(self, data): """``data = [<id de l'activité>, <format de sortie>]`` Transmet la liste des invites d'une activités. Le format de sortie peut être : * ``"python"`` : la liste en format python, (même chose que get_invites) * ``"html"`` : un tableau en html * ``"pdf"`` : """ if not (type(data) == list and len(data) == 2 and type(data[0]) == int and type(data[1]) == unicode): _badparam(self, u"liste_invites") return [idact, typ] = data if not typ in [u"python", u"html", u"pdf"]: _badparam(self, u"liste_invites (type de liste inconnu : %r)" % (typ)) return if self._has_acl("invites_admin"): con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM activites WHERE id = %s;", (idact,)) activite = cur.fetchall() if activite: activite = activite[0] else: self._debug(3, u"liste_invites failed : activités %s inexistante" % (idact)) self._send(None, 404, u"Cette activité n'existe pas.") return l = _get_invites(idact, compute_times=True) if typ == u"python": self._send(l) self._debug(4, u"liste_invites (%s) par %s pour l'activité %s" % (typ, self.userid, idact)) return # On formate en html content = "".join([_make_html(config.html_invites_tr_template, {}, invite) for invite in l]) table = _make_html(config.html_table_template, {"table_header" : config.html_invites_th_template, "table_content" : content}, {"date" : time.strftime("%d/%m/%Y"), "heure" : time.strftime("%T"), "titre" : u"Liste d'invités pour %s" % (activite["titre"])}) if typ == u"html": self._send(table) self._debug(4, u"liste_invites (%s) par %s pour l'activité %s" % (typ, self.userid, idact)) return # On convertit en pdf avec wkhtmltopdf rawpdf, error = _make_pdf_from_html(table) if error: self._send(None, 740, error) self._debug(3, u"liste_invites failed : erreur à la génération du pdf") return self._send(base64.b64encode(rawpdf)) self._debug(4, u"liste_invites (%s) par %s pour l'activité %s" % (typ, self.userid, idact)) else: _pasledroit(self, "invites_admin")
[docs]def mayi(self, data): # May I """Permet de demander si on a tel ou tel droit. (Par exemple utile aux client pour afficher/masquer certaines fonctions dont l'utilisation ne serait pas autorisée.) * ``data = <un droit>`` : transmet si l'utilisateur a le droit ``<un droit>`` * ``data = "alive"`` : transmet si l'utilisateur est toujours logué (permet de vérifier qu'on n'a pas timeouté) * ``data = "pages"`` : transmet la liste des pages accessible à l'utilisateur * ``data = "droits"`` ou ``data = "rights"`` : transmet la liste des droits existants * ``data = "full_rights"`` : transmet dans un dictionnaire les listes (exhaustives) des droits et surdroits de l'utilisateur """ if not(type(data) == unicode): _badparam(self, u"mayi") return if (data == "pages"): # il y a déjà une fonction qui donnes les pages auxquelles on a le droit d'accéder self._debug(5, u"mayi asked : pages.") return django_get_accessible_pages(self) if data in ["droits", "rights"]: # on demande les droits existants tosend = copy.deepcopy(config.droits_aliases_bdd) # on ajoute les clés dans le bon ordre tosend["_keys"] = config.droits_aliases_bdd_keys self._debug(5, u"mayi asked : %s (answer %s)" % (data, tosend)) self._send(tosend) return if (data == "alive"): # On vérifie que la session n'a pas timeout tosend = not self._has_timeouted("alive") self._send(tosend) self._debug(5, u"mayi asked : %s (answer %s)" % (data, tosend)) return if (data == "full_rights"): # on demande ses propres droits mais de manière exhausitve ( = en développant les alias) dicoalias = {True: config.droits_aliases_special, False: config.droits_aliases_bdd}[self.userid == "special"] all_rights = ["all"] + config.droits_aliases_bdd_keys + sum(dicoalias.values(), []) # on regarde si il a chaque droit successivement got_rights = [droit for droit in all_rights if self._has_acl(droit)] if (self.userid == "special"): self._send({"droits": got_rights}) return # pour un non special, on rajoute les surdroits got_overrights = [droit for droit in all_rights if self._has_acl(droit, surdroit=True)] tosend = {"droits": got_rights, "surdroits": got_overrights} self._send(tosend) self._debug(5, u"mayi asked : %s (answer %s)" % (data, tosend)) return hemay = self._has_acl(data) self._send(hemay) self._debug(5, u"mayi asked : %s (answer %s)" % (data, hemay))
[docs]def historique_transactions(self, data): """Renvoie l'historique des transactions de : * ``data = [idbde, num_page, nb]`` : les ``nb`` consos de la page n°``num_page`` du compte ``idbde`` * ``data = "last"`` : les consos récentes * ``data = ["last", begin, nb]`` : les ``nb`` dernières consos à partir de la n°``begin`` Dans le cas ``"last"``, renvoie une liste de transactions. Dans le cas ``idbde``, renvoie un dictionnaire contenant les champs suivants : * ``"historique"`` : la liste des transactions * ``"nb_transactions"`` : le nombre total de transactions pour ce compte * ``"nb_pages"`` : le nombre de pages que ça prendrait en les affichant à ``nb`` par page * ``"num_page"`` : le numéro de la page effectivement envoyée (si on charge un numéro trop élevé/négatif, on est ramené à la page max/min) """ if (type(data) == list) and [type(i) for i in data] == [int, int, int]: idbde, num_page, nb = data if self._has_acl("historique_transactions") or self._myself(idbde): con, cur = BaseFonctions.getcursor() # On cherche le nombre de transactions rattachées à ``idbde`` cur.execute("""SELECT count(*) FROM transactions WHERE %s IN (emetteur,destinataire) ;""", (idbde,)) nb_lignes = cur.fetchone() # On calcule le nombre de pages nécéssaires pour afficher l'historique nb_pages = nb_lignes[0] // nb if nb_lignes[0] % nb != 0 : nb_pages += 1 # Si on essaie de charger un numéro de page qui n'existe pas, on redirige vers la plus proche if nb_pages == 0: # cas particulier d'absence de transactions num_page = 1 else: num_page = min(max(num_page, 1), nb_pages) begin = nb * (num_page - 1) # On demande l'historique de ``idbde`` cur.execute("""SELECT t.*, cem.pseudo AS emetteurpseudo, cdest.pseudo AS destinatairepseudo FROM transactions AS t, comptes AS cem, comptes AS cdest WHERE t.emetteur = cem.idbde AND t.destinataire = cdest.idbde AND %s in (t.emetteur, t.destinataire) ORDER BY date DESC LIMIT %s OFFSET %s;""", (idbde, nb, begin,)) historique = cur.fetchall() l = { 'historique' : historique, 'nb_transactions' : nb_lignes[0], 'nb_pages' : nb_pages, 'num_page' : num_page, } self._send(l) self._debug(4, u"envoi de l'historique et du nombre de transactions de %s" % (idbde,)) else: _pasledroit(self, "historique_transactions") elif (type(data) in [unicode, list]): if type(data) == list and [type(i) for i in data] == [unicode, int, int] and data[0] == "last": begin, nb = data[1], data[2] elif data == "last": begin, nb = 0, config.buffer_transactions_size else: _badparam(self, u"historique_transactions") return if self._has_acl("consos"): con, cur = BaseFonctions.getcursor() cur.execute("""SELECT t.*, cem.pseudo AS emetteurpseudo, cdest.pseudo AS destinatairepseudo FROM transactions AS t, comptes AS cem, comptes AS cdest WHERE t.emetteur = cem.idbde AND t.destinataire = cdest.idbde ORDER BY id DESC LIMIT %s OFFSET %s;""", (nb, begin)) l = cur.fetchall() self._send(l) self._debug(4, u"envoi des transactions récentes (nb : %s, offset : %s)" % (nb, begin)) else: _pasledroit(self, "consos") else: _badparam(self, u"historique_transactions")
[docs]def _can_toggle_transaction(self, transaction): """Dit si l'utilisateur a le droit de changer le statut valide/invalide de la transaction. Cela dépend de ses droits, de la date de la transaction et si elle est cantinvalidate. La règle est la suivante : on peut valider/dévalider une transaction si on aurait le droit de faire la transaction que va effectuer cette validation/dévalidation. """ if transaction["cantinvalidate"]: self._debug(3, u"can't toggle transaction : transaction %s est en cantinvalidate" % transaction["id"]) self._send(None, 312, u"Cette transaction ne peut pas être validée/dévalidée.") return False if self._has_acl("transactions_admin"): # Quand on a ces droits (à distribuer parcimonieusement car aussi puissants que overforced) # on peut changer l'état d'une transaction sans condition return True con, cur = BaseFonctions.getcursor() cur.execute("SELECT toggle_transaction_timeout FROM configurations WHERE used = true;") duree = cur.fetchone()[0] if time.mktime(transaction["date"].timetuple()) < time.time() - duree: # une transaction trop vieille ne peut pas être touchée self._debug(3, u"can't toggle transaction : transaction %s trop ancienne" % transaction["id"]) self._send(None, 313, u"Cette transaction a été effectuée il y a trop longtemps.") return False # Ensuite on cherche à savoir si l'utilisateur aurait pu faire cette transaction forced, overforced = self._has_acl("forced"), self._has_acl("overforced") # Attention, si on cherche à dévalider, il faut bidouiller la transaction if transaction["valide"]: transaction = {k : v for (k, v) in transaction.iteritems()} transaction["emetteur"], transaction["destinataire"] = transaction["destinataire"], transaction["emetteur"] result, _ = _une_transaction(self, transaction["type"], transaction["emetteur"], transaction["destinataire"], transaction["quantite"], transaction["montant"], transaction["description"], transaction["categorie"], forced, overforced, justtesting=True) success = (result.split()[0] == "ok") if success: return True else: method = result.split()[-2] self._debug(3, u"can't toggle transaction : %s needed (id = %s)" % (method, transaction["id"])) self._send(None, 314, u"Tu n'as pas les droits suffisants (%s nécessaire)." % method) return False
[docs]def _valider_ou_devalider_transaction(self, idtransaction, validate): """ Si ``validate`` est ``True``, valide la transaction n°``idtransaction``, sinon, la dévalide. Répercute les effets sur les soldes. _log relevant ids : idtransaction """ cmd_name = (u"" if validate else u"de") + u"valider_transaction" if not type(idtransaction) == int: _badparam(self, cmd_name) return if self._has_acl("transactions"): # On commence par aller chercher la transaction en question con, cur = BaseFonctions.getcursor() cur.execute("SELECT * FROM transactions WHERE id = %s;", (idtransaction,)) transaction = cur.fetchone() if transaction is None: self._send(None, 404, u"Cette transaction n'existe pas.") self._debug(3, u"%s failed : id %s inconnu" % (cmd_name, idtransaction)) return if transaction["valide"] == validate: status = u"%svalidée" % (u"" if validate else u"dé") self._send(None, 310 if validate else 311, u"Cette transaction est déjà %s." % (status,)) self._debug(3, u"%s failed : transaction %s déjà %s" % (cmd_name, idtransaction, status)) return if _can_toggle_transaction(self, transaction): cout = transaction["montant"] * transaction["quantite"] idemetteur, iddestinataire = transaction["emetteur"], transaction["destinataire"] if validate: a_debiter, a_crediter = idemetteur, iddestinataire else: # Pour *dé*valider, on fait le contraire a_debiter, a_crediter = iddestinataire, idemetteur # Débit d'un côté cur.execute("UPDATE comptes SET solde = solde - %s WHERE idbde = %s;", (cout, a_debiter)) # Crédit de l'autre cur.execute("UPDATE comptes SET solde = solde + %s WHERE idbde = %s;", (cout, a_crediter)) # On change la validité de la transaction cur.execute("UPDATE transactions SET valide = NOT valide WHERE id = %s;", (idtransaction,)) self._log(cmd_name, cur, idtransaction, [idtransaction]) cur.execute("COMMIT;") newstatus = u"%svalidée" % (u"" if validate else u"dé") self._debug(1, u"transaction %s %s" % (idtransaction, newstatus)) self._send("Transaction %s." % (newstatus,)) # pas de else, dans le cas où ça n'a pas marché, _can_toggle_transaction s'est occupé du message d'erreur else: _pasledroit(self, "transactions")
[docs]def valider_transaction(self, data): """Valide une transaction qui était invalidée. Répercute les effets sur les soldes.""" _valider_ou_devalider_transaction(self, data, True)
[docs]def devalider_transaction(self, data): """Invalide une transaction qui était validée. Répercute les effets sur les soldes.""" _valider_ou_devalider_transaction(self, data, False)
[docs]def _factorize_idbde(liste): """ Prend en entrée une liste d'``idbde`` On renvoie une liste de couple ``(idbde,count(idbde))`` """ out = [] for idbde in set(liste): out.append((idbde,liste.count(idbde))) return out
[docs]def liste_droits(self): """ Renvoie un dico contenant les personnes ayant des droits differents de juste basic. """ #On vérifie qu'on a les droits if self._has_acl("liste_droits"): con, cur = BaseFonctions.getcursor() cur.execute(""" SELECT nom, prenom, pseudo, droits, surdroits, supreme, idbde, fonction FROM comptes WHERE (droits NOT IN ('basic', '') OR supreme OR surdroits != '') AND type != 'club' AND idbde > 0 AND idbde != 3508 AND NOT deleted ORDER BY nom, prenom; """) l = cur.fetchall() #On récupère l'idbde des personnes n'étant plus au BDE (on suppose que la passation se fait au 1er mars) annee, mois = datetime.datetime.now().year, datetime.datetime.now().month # Récupère l'année et le mois actuel anneejeune = annee - 1 - 1*(mois < 3) # Arrivée l'année précédente si on est après mars, deux ans avant sinon cur.execute("SELECT idbde FROM adhesions WHERE annee < {};".format(anneejeune)) m = cur.fetchall() # On renvoie par la socket (le trou) le résultat en le mettant en forme avec la fonction _send() self._send([l,m]) self._debug(4, u"Liste des gens avec des droits surnormaux envoyée.") else: # Si on a pas le droit, on renvoie par la socket un message d'erreur qui sera traité dans la view _pasledroit(self, "liste_droits")
################################################################# ## Fonctions spéciales client Django ## #################################################################
[docs]def django_get_accessible_pages(self, liste_pages): """Donne la liste des pages accessibles en fonction des droits de l'utilisateur""" pages = [[page, adresse] for (page, adresse, droit) in liste_pages if self._has_acl(droit, sousdroit=True)] self._send(pages) self._debug(4, u"Envoi des pages autorisées")