models.py 8.73 KB
Newer Older
Daniel STAN's avatar
Daniel STAN committed
1 2
# -*- coding: utf-8 -*-

3
from django.contrib import messages
Mathilde Espinasse's avatar
Mathilde Espinasse committed
4
from django.utils.translation import ugettext_lazy as _
5

6
from intranet import settings, conn_pool
7 8 9 10 11
class asterisk_reload_conf_debug:
    @classmethod
    def reload_config(self, fname):
        pass

12
# TODO weird stuff here
13
try:
14
    from sip import asterisk_reload_conf
15 16 17 18
except ImportError:
    if not settings.DEBUG:
        raise
    asterisk_reload_conf = asterisk_reload_conf_debug
19

Daniel STAN's avatar
Daniel STAN committed
20 21
from django.db import models
from django.contrib.auth.models import User
22
import hashlib
23
import os
24

25
# Description de la configuration générée pour asterisk
26
# TODO eurk
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
asterisk_conf = {
    'modules':{
        'sip':{
            'reload_fields':["caller_id","password"],
            'gen_conf':lambda _:Profile.asterisk_config, # pour pouvoir appeler Profile paresseusement
            'conf_file':'sip_accounts',
        },
        'voicemail':{
            'reload_fields':["voicemail_password"],
            'gen_conf':lambda _:Profile.asterisk_voicemail_config,
            'conf_file':'sip_accounts_voicemail',
        },
        'dialplan':{
            'reload_fields':["num"],
            'gen_conf':lambda _:Profile.asterisk_alias_config,
            'conf_file':'sip_accounts_alias',
        },
   },
   'conf_path':'/usr/scripts/var/sip/'
}


Daniel STAN's avatar
Daniel STAN committed
49 50 51 52
class Profile(models.Model):
    """ Profil d'utilisateur Voip """

    class Meta:
Mathilde Espinasse's avatar
Mathilde Espinasse committed
53 54
        verbose_name = _(u'Profil SIP')
        verbose_name_plural = _(u'Profils SIP')
55

Daniel STAN's avatar
Daniel STAN committed
56
    CALLER_IDS = [
Mathilde Espinasse's avatar
Mathilde Espinasse committed
57 58 59
        ('full_name', _(u'Nom complet')),
        ('number', _(u'Numéro de Téléphone')),
        ('both', _(u'Nom et numéro de téléphone')),
Daniel STAN's avatar
Daniel STAN committed
60
    ]
61

62
    """ Propriétaire du profil. Devrait être un compte Cr@ns """
Daniel STAN's avatar
Daniel STAN committed
63
    user = models.OneToOneField(User, primary_key=True)
64 65 66

    """ Mot de passe. Hashé en md5 avec un sel
        (md5($num_asterisk:asterisk:$pass)) """
Mathilde Espinasse's avatar
Mathilde Espinasse committed
67
    password = models.CharField(_(u"Mot de passe"), max_length=32, null=False, blank=False)
68 69

    """ Autoriser ou non la publication dans l'annuaire """
Mathilde Espinasse's avatar
Mathilde Espinasse committed
70
    published = models.BooleanField(_(u"Publication dans l'annuaire"), help_text=u"Affichage du numéro et du nom dans l'annuaire du Crans", default=False)
71 72

    """ Choix du format pour l'identification de l'appelant """
Mathilde Espinasse's avatar
Mathilde Espinasse committed
73
    caller_id = models.CharField(_(u"Affichage de l'appelant"), max_length=9, choices=CALLER_IDS, help_text=u"Nom à afficher lors d'un appel")
74

75
    """ Numéro de téléphone, on utilise un varchar afin de pouvoir placer des 0
76
        initiaux et des éventuels #"""
Mathilde Espinasse's avatar
Mathilde Espinasse committed
77 78
    num = models.CharField(_(u"Numéro de téléphone"), max_length=10, null=False, blank=False)
    voicemail_password = models.CharField(_(u"Mot de passe boîte vocale"), max_length=6, null=False, blank=False)
79

80 81 82 83 84 85 86 87 88 89 90 91

    def what_to_reload(self):
        """ Defini les modules asterisk dont il faut régénérer la configuration """
        def has_changed(self, field):
            if not self.pk:
                return True
            try:
                old_value = self.__class__._default_manager.\
                         filter(pk=self.pk).values(field).get()[field]
            except self.DoesNotExist:
                return True
            return not getattr(self, field) == old_value
Daniel STAN's avatar
Daniel STAN committed
92

93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
        modules={}
        for module in asterisk_conf['modules'].keys():
            modules[module]=reduce( # List.fold_left
                lambda x,y: x or has_changed(self,y),
                asterisk_conf['modules'][module]['reload_fields'],
                False
            )
        return modules

    def reload_what_it_is_to_reload(self,modules):
        """ Écrit la configuration des modules asterisk à recharger, puis les recharge """
        def writeconf(gen_conf_module,path):
            conf=""
            for profile in Profile.objects.order_by('user__username').all():
                conf+=gen_conf_module(profile).encode('utf-8') + '\n'
108
            tmppath = path + '.new'
Mathilde Espinasse's avatar
Mathilde Espinasse committed
109
            if not settings.DEBUG:
110 111 112
                with open(tmppath,'w') as fout:
                    fout.write(conf)
                os.rename(tmppath, path)
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
        for module in asterisk_conf['modules'].keys():
            if modules[module]:
                writeconf(
                    asterisk_conf['modules'][module]['gen_conf'](''),
                    asterisk_conf['conf_path'] + asterisk_conf['modules'][module][ 'conf_file']
                )
        
        if reduce(lambda x,y: x or y, modules.values(), False):
            asterisk_reload_conf.reload_config('all')
        else:
            for module in asterisk_conf['modules'].keys(): 
                if modules[module]:
                    asterisk_reload_conf.reload_config(module)

    def delete(self):
        ret=super(Profile,self).delete()
        self.reload_what_it_is_to_reload(self.what_to_reload())
        return ret

    def save(self):
        to_reload=self.what_to_reload()
        ret=super(Profile,self).save()
        self.reload_what_it_is_to_reload(to_reload)
        return ret

138
    def set_user(self, new_user):
139 140 141 142
        """ Défini l'utilisateur propriétaire du profil. Cette fonction calcule
        également le numéro de téléphone correspondant, à partir de la base LDAP.
        IMPORTANT: changer d'utilisateur (donc de numéro de téléphone) casse
        le mot de passe, qui devra être redéfini."""
143 144
        self.user = new_user
        try:
145
            aid = conn_pool.get_user(new_user)['aid'][0].value
146
            sip_ext = '1%04d' % int(str(aid)[:4])
147
        except KeyError:
148 149 150
            if not settings.DEBUG:
                raise Exception("Not allowed") # FIXME ?
            sip_ext = '9%04d' % int(str(new_user.pk)[:4])
151 152 153
        self.num = sip_ext

    def set_password(self, new_pass):
154 155 156 157 158 159 160
        """ Définition du mot de passe.
        IMPORTANT: le mot de passe est stocké hashé dans la base de donnée.
        Il faut donc avoir préalablement défini le numéro de téléphone (et
        donc l'utilisateur) avant d'appeler cette fonction.
        """
        if self.num == '':
            raise Exception('Set user/num first')
161
        self.password = hashlib.md5("%s:asterisk:%s" % (self.num, new_pass)).hexdigest()
Daniel STAN's avatar
Daniel STAN committed
162 163

    def __unicode__(self):
Mathilde Espinasse's avatar
Mathilde Espinasse committed
164 165 166 167 168 169
        #return _(u"Profil de %s (dont le numéro est %s)") % (self.user.username, self.num)
        return _(u"Profil de %(login)s (dont le numéro est %(numero)s)") % \
           {
              'login': self.user.username,
              'numero': self.num
           }
Daniel STAN's avatar
Daniel STAN committed
170

171 172 173 174
    def asterisk_alias_config(self):
        """Génération de la ligne de conf asterisk mappant login à son numéro correspondant"""
        return "exten => %(login)s,1,Goto(%(num)s,1)" % {'num':self.num,'login':self.user.username.split('@')[0]}

175
    def asterisk_voicemail_config(self):
176
        """ Génération de la ligne de conf asterisk activant la boîte vocale"""
177
        return u"%(num)s => %(mail_pass)s,%(full_name)s,%(mail)s,,attach=%(attach)s|delete=%(delete)s" % {
178
       'num':self.num,'mail_pass':self.voicemail_password,'full_name':self.user.get_full_name(),
179 180 181
       'mail':self.user.username,'attach':'yes','delete':'no'}

    def asterisk_config(self):
182
        """ Génération du paragraphe de conf asterisk definissant l'utilisateur"""
183
        return u""";%(uid)s
184
[%(num)s](default-crans)
185 186 187 188 189 190 191 192 193 194 195
username=%(num)s
callerid="%(callerid)s"
md5secret=%(passwd)s
mailbox=%(num)s@666
""" % {
 'callerid': self.get_caller_id(),
 'num': self.num,
 'passwd': self.password,
 'uid': self.user.username,
}

196

197 198 199
    def get_category(self):
        """ Renvoie la catégorie du profil, dans l'annuaire """
        return "Cr@ns"
200 201

    def get_caller_id(self):
202
        """ Calcule le texte descriptif de l'appelant """
203 204 205 206 207 208
        if self.caller_id == 'full_name':
            return self.user.get_full_name()
        elif self.caller_id == 'number':
            return self.num
        else: # Both
            return self.user.get_full_name() + u' <' + self.num + u'>'
Daniel STAN's avatar
Daniel STAN committed
209 210 211 212 213 214 215 216 217

class Call(models.Model):
    """ Un appel archivé.
    Automatiquement rempli par asterisk (et les scripts maisons de Nit)
    """

    class Meta:
        """La table s'appelle history, on définit ici plutôt un nom pour un objet"""
        db_table = 'voip_history'
Mathilde Espinasse's avatar
Mathilde Espinasse committed
218 219
        verbose_name = _(u'Appel archivé')
        verbose_name_plural = _(u'Appels archivés')
Daniel STAN's avatar
Daniel STAN committed
220

Mathilde Espinasse's avatar
Mathilde Espinasse committed
221 222 223
    uniq_id = models.CharField(_(u"Identifiant de l'appel"), max_length=80, primary_key=True)
    src = models.CharField(_(u"Numéro appelant"), max_length=80)
    dst = models.CharField(_(u"Numéro appelant"), max_length=80)
Daniel STAN's avatar
Daniel STAN committed
224 225

    """ Une durée égale à NULL correspond à un appel en cours (sauf si plantage) """
Mathilde Espinasse's avatar
Mathilde Espinasse committed
226 227
    duration = models.IntegerField(_(u"Durée de l'appel"), null=True, blank=True)
    date = models.DateField(_(u"Date d'initiation de l'appel"))
Daniel STAN's avatar
Daniel STAN committed
228 229

    def __unicode__(self):
230 231 232 233 234
        o = u"%s -> %s @ %s" % (self.src, self.dst, self.date)
        if self.duration:
            o += " (%ds)" % self.duration
        return o