models.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # -*- coding: utf-8 -*-
  2. import ldapdb.models
  3. import pprint
  4. import os
  5. import base64
  6. import hashlib
  7. import unicodedata
  8. import string
  9. import datetime
  10. from django.db import models
  11. from django.db.models import Q
  12. from django.db.models.signals import post_save, pre_save, post_delete
  13. from django.dispatch import receiver
  14. from django.core import exceptions
  15. from ldapdb.models.fields import CharField, IntegerField, ListField
  16. from south.modelsinspector import add_ignored_fields
  17. from coin.offers.models import OfferSubscription
  18. from coin.models import CoinLdapSyncModel
  19. from django.contrib.auth.signals import user_logged_in
  20. from django.conf import settings
  21. class Member(CoinLdapSyncModel):
  22. MEMBER_TYPE_CHOICES = (
  23. ('personne_physique', 'Personne physique'),
  24. ('personne_morale', 'Personne morale'),
  25. )
  26. MEMBER_STATUS_CHOICES = (
  27. ('adherent', 'Adhérent'),
  28. ('non_adherent', 'Non adhérent'),
  29. ('demande_adhesion', "Demande d'adhésion"),
  30. )
  31. user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, default=None,
  32. verbose_name='Utilisateur Django',
  33. on_delete=models.SET_NULL)
  34. status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
  35. default='non_adherent')
  36. type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
  37. default='personne_physique')
  38. first_name = models.CharField(max_length=200, verbose_name=u'Prénom')
  39. last_name = models.CharField(max_length=200, verbose_name=u'Nom')
  40. ldap_cn = models.CharField(max_length=200, blank=True,
  41. help_text='Clé avec le LDAP. Laisser vide pour '
  42. 'la générer automatiquement')
  43. organization_name = models.CharField(max_length=200, blank=True,
  44. verbose_name='Nom de l\'organisme',
  45. help_text='Pour une personne morale')
  46. email = models.EmailField(max_length=254, verbose_name=u'Courriel')
  47. home_phone_number = models.CharField(max_length=25, blank=True,
  48. verbose_name=u'Téléphone fixe')
  49. mobile_phone_number = models.CharField(max_length=25, blank=True,
  50. verbose_name=u'Téléphone mobile')
  51. address = models.TextField(verbose_name=u'Adresse')
  52. postal_code = models.CharField(max_length=15,
  53. verbose_name=u'Code postal')
  54. city = models.CharField(max_length=200,
  55. verbose_name=u'Commune')
  56. country = models.CharField(max_length=200,
  57. default='France',
  58. verbose_name=u'Pays')
  59. entry_date = models.DateField(null=False,
  60. blank=False,
  61. default=datetime.date.today,
  62. verbose_name='Date de première adhésion')
  63. resign_date = models.DateField(null=True, blank=True,
  64. verbose_name='Date de départ de '
  65. 'l\'association')
  66. def __unicode__(self):
  67. name = self.first_name + ' ' + self.last_name
  68. if (self.organization_name):
  69. name += ' (%s)' % self.organization_name
  70. return name
  71. # Renvoi la date de fin de la dernière cotisation du membre
  72. def end_date_of_membership(self):
  73. try:
  74. return self.membership_fees.order_by('-end_date')[0].end_date
  75. except:
  76. return None
  77. def change_password(self, new_password):
  78. ldap_user = LdapUser.objects.get(pk=self.ldap_cn)
  79. ldap_user.password = new_password
  80. ldap_user.save()
  81. def get_active_subscriptions(self, date=datetime.date.today()):
  82. return OfferSubscription.objects.filter(
  83. Q(member__exact=self.pk),
  84. Q(subscription_date__lte=date),
  85. Q(resign_date__isnull=True) | Q(resign_date__gte=date))
  86. def get_automatic_ldap_cn(self):
  87. """
  88. Calcul le login / ldap_cn automatiquement en fonction
  89. du nom et du prénom
  90. """
  91. # Première lettre de chaque partie du prénom
  92. first_name_letters = ''.join(
  93. [c[0] for c in self.first_name.split('-')]
  94. )
  95. # Concaténer avec nom de famille
  96. ldap_cn = ('%s%s' % (first_name_letters, self.last_name))
  97. # Remplacer ou enlever les caractères non ascii
  98. ldap_cn = unicodedata.normalize('NFD', ldap_cn)\
  99. .encode('ascii', 'ignore')
  100. # Enlever ponctuation et espace
  101. ldap_cn = ldap_cn.translate(None, string.punctuation + ' ')
  102. # En minuscule
  103. ldap_cn = ldap_cn.lower()
  104. return ldap_cn
  105. def sync_to_ldap(self, creation):
  106. """
  107. Update LDAP data when a member is saved
  108. """
  109. assert self.ldap_cn, ('Can\'t sync with LDAP because missing ldap_cn '
  110. 'value for the Member : %s' % self)
  111. if not creation:
  112. ldap_user = LdapUser.objects.get(pk=self.ldap_cn)
  113. if creation:
  114. max_uidNumber = LdapUser.objects.order_by('-uidNumber')[0].uidNumber
  115. ldap_user = LdapUser()
  116. ldap_user.pk = self.ldap_cn
  117. ldap_user.uid = self.ldap_cn
  118. ldap_user.nick_name = self.ldap_cn
  119. ldap_user.uidNumber = max_uidNumber + 1
  120. ldap_user.last_name = self.last_name
  121. ldap_user.first_name = self.first_name
  122. ldap_user.save()
  123. if creation:
  124. ldap_group = LdapGroup.objects.get(pk='coin')
  125. ldap_group.members.append(ldap_user.pk)
  126. ldap_group.save()
  127. class Meta:
  128. verbose_name = 'membre'
  129. class CryptoKey(models.Model):
  130. KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
  131. type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES)
  132. key = models.TextField(verbose_name=u'Clé')
  133. member = models.ForeignKey('Member', verbose_name=u'Membre')
  134. def __unicode__(self):
  135. return u'Clé %s de %s' % (self.type, self.member)
  136. class Meta:
  137. verbose_name = 'clé'
  138. class MembershipFee(models.Model):
  139. member = models.ForeignKey('Member', related_name='membership_fees',
  140. verbose_name=u'Membre')
  141. amount = models.IntegerField(null=False, default='20', help_text='en €',
  142. verbose_name=u'Montant')
  143. start_date = models.DateField(
  144. null=False,
  145. blank=False,
  146. default=datetime.date.today,
  147. verbose_name='Date de début de cotisation')
  148. end_date = models.DateField(
  149. null=False,
  150. blank=False,
  151. default=datetime.date.today() + datetime.timedelta(365),
  152. verbose_name='Date de fin de cotisation')
  153. def __unicode__(self):
  154. return (u'%s - %s - %i€' % (self.member, self.start_date,
  155. self.amount))
  156. class Meta:
  157. verbose_name = 'cotisation'
  158. class LdapUser(ldapdb.models.Model):
  159. # TODO: déplacer ligne suivante dans settings.py
  160. base_dn = "ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  161. object_classes = ['inetOrgPerson', 'organizationalPerson', 'person',
  162. 'top', 'posixAccount']
  163. uid = CharField(db_column='uid', unique=True, max_length=255)
  164. nick_name = CharField(db_column='cn', unique=True, primary_key=True,
  165. max_length=255)
  166. first_name = CharField(db_column='givenName', max_length=255)
  167. last_name = CharField(db_column='sn', max_length=255)
  168. display_name = CharField(db_column='displayName', max_length=255,
  169. blank=True)
  170. password = CharField(db_column='userPassword', max_length=255)
  171. uidNumber = IntegerField(db_column='uidNumber', unique=True)
  172. gidNumber = IntegerField(db_column='gidNumber', default=2000)
  173. homeDirectory = CharField(db_column='homeDirectory', max_length=255,
  174. default='/tmp')
  175. def __unicode__(self):
  176. return self.display_name
  177. class Meta:
  178. managed = False # Indique à South de ne pas gérer le model LdapUser
  179. class LdapGroup(ldapdb.models.Model):
  180. """
  181. Class for representing an LDAP group entry.
  182. """
  183. # LDAP meta-data
  184. base_dn = "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  185. object_classes = ['posixGroup']
  186. # posixGroup attributes
  187. gid = IntegerField(db_column='gidNumber', unique=True)
  188. name = CharField(db_column='cn', max_length=200, primary_key=True)
  189. members = ListField(db_column='memberUid')
  190. def __unicode__(self):
  191. return self.name
  192. class Meta:
  193. managed = False # Indique à South de ne pas gérer le model LdapGroup
  194. # Indique à South de ne pas gérer les models LdapUser et LdapGroup
  195. add_ignored_fields(["^ldapdb\.models\.fields"])
  196. @receiver(pre_save, sender=Member)
  197. def define_ldap_cn(sender, instance, **kwargs):
  198. """
  199. Lors de la sauvegarde d'un membre. Si le champ ldap_cn n'est pas définit,
  200. le calcul automatiquement en fonction du nom et du prénom
  201. """
  202. if not instance.ldap_cn and not instance.pk:
  203. instance.ldap_cn = instance.get_automatic_ldap_cn()
  204. @receiver(pre_save, sender=LdapUser)
  205. def change_password(sender, instance, **kwargs):
  206. """
  207. Lors de la sauvegarde d'un utilisateur Ldap, cette fonction est exécutée
  208. avant la sauvegarde pour chiffrer le mot de passe s'il est définit
  209. et s'il n'est pas déjà chiffré
  210. """
  211. # Si le mot de passe est définit et n'est pas déjà chiffré,
  212. # alors ça le chiffre
  213. if instance.password and not instance.password.startswith('{SSHA}'):
  214. salt = os.urandom(8).encode('hex')
  215. digest = hashlib.sha1(instance.password + salt).digest()
  216. instance.password = '{SSHA}' + base64.b64encode(digest + salt)
  217. @receiver(pre_save, sender=LdapUser)
  218. def define_display_name(sender, instance, **kwargs):
  219. """
  220. Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
  221. concaténation de first_name et last_name
  222. """
  223. if not instance.display_name:
  224. instance.display_name = '%s %s' % (instance.first_name,
  225. instance.last_name)
  226. @receiver(post_delete, sender=Member)
  227. def remove_ldap_user_from_coin_group_when_deleting_member(sender,
  228. instance,
  229. **kwargs):
  230. """
  231. Lorsqu'un membre est supprimé du SI, son utilisateur LDAP correspondant est
  232. sorti du groupe "coin"
  233. """
  234. ldap_group = LdapGroup.objects.get(pk='coin')
  235. if instance.ldap_cn in ldap_group.members:
  236. ldap_group.members.remove(instance.ldap_cn)
  237. ldap_group.save()
  238. @receiver(user_logged_in)
  239. def define_member_user(sender, request, user, **kwargs):
  240. """
  241. Lorsqu'un utilisateur se connect avec succes, fait le lien entre le membre
  242. et l'utilisateur en définissant le champ user du model membre ayant le
  243. ldap_cn utilisé pour la connexion
  244. """
  245. try:
  246. member = Member.objects.get(ldap_cn=user.username)
  247. if not member.user:
  248. member.user = user
  249. member.save()
  250. elif member.user.username != user.username:
  251. raise Exception('Un membre avec cet ldap_cn existe en base de '
  252. 'donnée mais l\'utilisateur auquel il est rattaché '
  253. 'ne correspond pas.')
  254. except Member.DoesNotExist:
  255. if not user.is_superuser:
  256. raise
  257. #==============================================================================
  258. # @receiver(pre_save, sender = LdapUser)
  259. # def ssha_password(sender, **kwargs):
  260. # if not kwargs['instance'].password.startswith('{SSHA}'):
  261. # salt = os.urandom(8).encode('hex')
  262. # kwargs['instance'].password = '{SSHA}' + base64.b64encode(
  263. # hashlib.sha1(obj.password + salt).digest() + salt)
  264. #==============================================================================