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