models.py 12 KB

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